Browse Source

Exchanges: Remove XMR.to

bruteforce
tobtoht 9 months ago
parent
commit
9e96f4183a
Signed by: tobtoht GPG Key ID: 1CADD27F41F45C3C
  1. 1
      CMakeLists.txt
  2. 1
      HACKING.md
  3. 1
      Makefile
  4. 4
      src/CMakeLists.txt
  5. 19
      src/appcontext.cpp
  6. 3
      src/appcontext.h
  7. 3
      src/assets.qrc
  8. BIN
      src/assets/images/exchange_white.png
  9. 35
      src/dialog/xmrtoinfodialog.cpp
  10. 28
      src/dialog/xmrtoinfodialog.h
  11. 256
      src/dialog/xmrtoinfodialog.ui
  12. 27
      src/mainwindow.cpp
  13. 39
      src/mainwindow.ui
  14. 107
      src/model/XmrToModel.cpp
  15. 38
      src/model/XmrToModel.h
  16. 110
      src/utils/xmrto.cpp
  17. 63
      src/utils/xmrto.h
  18. 76
      src/utils/xmrtoapi.cpp
  19. 87
      src/utils/xmrtoapi.h
  20. 274
      src/utils/xmrtoorder.cpp
  21. 106
      src/utils/xmrtoorder.h
  22. 193
      src/xmrtowidget.cpp
  23. 67
      src/xmrtowidget.h
  24. 327
      src/xmrtowidget.ui

1
CMakeLists.txt

@ -10,7 +10,6 @@ set(VERSION_REVISION "0")
set(VERSION "beta-3")
option(FETCH_DEPS "Download dependencies if they are not found" ON)
option(XMRTO "Include Xmr.To module" ON)
option(XMRIG "Include XMRig module" ON)
option(TOR_BIN "Path to Tor binary to embed inside Feather" OFF)

1
HACKING.md

@ -41,7 +41,6 @@ via the `CMAKE_PREFIX_PATH` definition. For me this is:
There are some Monero/Feather related options/definitions that you may pass:
- `-DXMRTO=OFF` - disable Xmr.To feature
- `-DXMRIG=OFF` - disable XMRig feature
- `-DTOR_BIN=/path/to/tor` - Embed a Tor executable inside Feather
- `-DDONATE_BEG=OFF` - disable the dreaded donate requests

1
Makefile

@ -30,7 +30,6 @@ CMAKEFLAGS = \
-DARCH=x86_64 \
-DBUILD_64=On \
-DBUILD_TESTS=Off \
-DXMRTO=On \
-DXMRIG=On \
-DTOR_BIN=Off \
-DCMAKE_CXX_STANDARD=11 \

4
src/CMakeLists.txt

@ -122,10 +122,6 @@ if(DONATE_BEG)
target_compile_definitions(feather PRIVATE DONATE_BEG=1)
endif()
if(XMRTO)
target_compile_definitions(feather PRIVATE HAS_XMRTO=1)
endif()
if(TOR_BIN)
target_compile_definitions(feather PRIVATE HAS_TOR_BIN=1)
endif()

19
src/appcontext.cpp

@ -132,11 +132,6 @@ AppContext::AppContext(QCommandLineParser *cmdargs) {
// fiat/crypto lookup
AppContext::prices = new Prices();
// xmr.to
#ifdef HAS_XMRTO
this->XMRTo = new XmrTo(this);
#endif
// XMRig
#ifdef HAS_XMRIG
this->XMRig = new XmRig(this->configDirectory, this);
@ -171,7 +166,6 @@ void AppContext::initTor() {
if (m_wsUrl.host().endsWith(".onion"))
this->ws->webSocket.setProxy(*networkProxy);
}
}
void AppContext::initWS() {
@ -199,13 +193,6 @@ void AppContext::onSweepOutput(const QString &keyImage, QString address, bool ch
this->currentWallet->createTransactionSingleAsync(keyImage, address, outputs, this->tx_priority);
}
void AppContext::onCreateTransaction(XmrToOrder *order) {
// tx creation via xmr.to
const QString description = QString("XmrTo order %1").arg(order->uuid);
quint64 amount = WalletManager::amountFromDouble(order->incoming_amount_total);
this->onCreateTransaction(order->receiving_subaddress, amount, description, false);
}
void AppContext::onCreateTransaction(const QString &address, quint64 amount, const QString &description, bool all) {
// tx creation
this->tmpTxDescription = description;
@ -434,12 +421,6 @@ void AppContext::onWSMessage(const QJsonObject &msg) {
QJsonObject fiat_rates = msg.value("data").toObject();
AppContext::prices->fiatPricesReceived(fiat_rates);
}
#if defined(HAS_XMRTO)
else if(cmd == "xmrto_rates") {
auto xmr_rates = msg.value("data").toObject();
this->XMRTo->onRatesReceived(xmr_rates);
}
#endif
else if(cmd == "reddit") {
QJsonArray reddit_data = msg.value("data").toArray();
this->onWSReddit(reddit_data);

3
src/appcontext.h

@ -14,7 +14,6 @@
#include "utils/prices.h"
#include "utils/networking.h"
#include "utils/tor.h"
#include "utils/xmrto.h"
#include "utils/xmrig.h"
#include "utils/wsclient.h"
#include "utils/txfiathistory.h"
@ -81,7 +80,6 @@ public:
Tor *tor{};
WSClient *ws;
XmrTo *XMRTo;
XmRig *XMRig;
Nodes *nodes;
static Prices *prices;
@ -114,7 +112,6 @@ public:
public slots:
void onOpenWallet(const QString& path, const QString &password);
void onCreateTransaction(const QString &address, quint64 amount, const QString &description, bool all);
void onCreateTransaction(XmrToOrder *order);
void onCreateTransactionMultiDest(const QVector<QString> &addresses, const QVector<quint64> &amounts, const QString &description);
void onCancelTransaction(PendingTransaction *tx, const QVector<QString> &address);
void onSweepOutput(const QString &keyImage, QString address, bool churn, int outputs) const;

3
src/assets.qrc

@ -34,6 +34,7 @@
<file>assets/images/cutexmrfox.png</file>
<file>assets/images/edit.png</file>
<file>assets/images/exchange.png</file>
<file>assets/images/exchange_white.png</file>
<file>assets/images/expired.png</file>
<file>assets/images/expired_icon.png</file>
<file>assets/images/eye1.png</file>
@ -97,8 +98,6 @@
<file>assets/images/unpaid.png</file>
<file>assets/images/update.png</file>
<file>assets/images/warning.png</file>
<file>assets/images/xmrto_big.png</file>
<file>assets/images/xmrto.png</file>
<file>assets/images/xmrig.ico</file>
<file>assets/images/xmrig.svg</file>
<file>assets/images/zoom.png</file>

BIN
src/assets/images/exchange_white.png

Before After
Width: 128  |  Height: 128  |  Size: 1000 B

35
src/dialog/xmrtoinfodialog.cpp

@ -1,35 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#include "xmrtoinfodialog.h"
#include "ui_xmrtoinfodialog.h"
XmrToInfoDialog::XmrToInfoDialog(XmrToOrder *oInfo, QWidget *parent)
: QDialog(parent)
, ui(new Ui::XmrToInfoDialog)
, m_oInfo(oInfo)
{
ui->setupUi(this);
ui->status->setText(XmrTo::stateMap[(OrderState) oInfo->state]);
ui->xmrto_id->setText(!oInfo->uuid.isEmpty() ? oInfo->uuid : "");
ui->error_code->setText(!oInfo->errorCode.isEmpty() ? oInfo->errorCode : "");
ui->error_msg->setText(!oInfo->errorMsg.isEmpty() ? oInfo->errorMsg : "");
ui->xmr_amount->setText(QString::number(oInfo->incoming_amount_total));
ui->btc_amount->setText(QString::number(oInfo->btc_amount));
ui->rate->setText(oInfo->incoming_price_btc > 0 ? QString::number(oInfo->incoming_price_btc) : "");
ui->xmr_txid->setText(oInfo->xmr_txid);
ui->xmr_address->setText(oInfo->receiving_subaddress);
ui->btc_txid->setText(oInfo->btc_txid);
ui->btc_address->setText(oInfo->btc_dest_address);
this->adjustSize();
}
XmrToInfoDialog::~XmrToInfoDialog() {
delete ui;
}

28
src/dialog/xmrtoinfodialog.h

@ -1,28 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#ifndef FEATHER_XMRTOINFODIALOG_H
#define FEATHER_XMRTOINFODIALOG_H
#include <QDialog>
#include "utils/xmrto.h"
namespace Ui {
class XmrToInfoDialog;
}
class XmrToInfoDialog : public QDialog
{
Q_OBJECT
public:
explicit XmrToInfoDialog(XmrToOrder *oInfo, QWidget *parent = nullptr);
~XmrToInfoDialog() override;
private:
Ui::XmrToInfoDialog *ui;
XmrToOrder *m_oInfo;
};
#endif //FEATHER_XMRTOINFODIALOG_H

256
src/dialog/xmrtoinfodialog.ui

@ -1,256 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>XmrToInfoDialog</class>
<widget class="QWidget" name="XmrToInfoDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>689</width>
<height>581</height>
</rect>
</property>
<property name="windowTitle">
<string>Xmr.to Order</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Status:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>XMR.to ID:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Error code:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="status">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="xmrto_id">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="error_code">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>XMR amount:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>BTC amount:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>Rate:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="xmr_amount">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="btc_amount">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="rate">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Message:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="error_msg">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>XMR.to address:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="xmr_address">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>XMR txid:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="xmr_txid">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_12">
<property name="text">
<string>BTC address:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="btc_address">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>BTC txid:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="btc_txid">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Copy support template</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

27
src/mainwindow.cpp

@ -97,22 +97,6 @@ MainWindow::MainWindow(AppContext *ctx, QWidget *parent) :
connect(ui->actionShow_debug_info, &QAction::triggered, this, &MainWindow::showDebugInfo);
connect(ui->actionOfficialWebsite, &QAction::triggered, [=] { Utils::externalLinkWarning(this, "https://featherwallet.org"); });
#if defined(HAS_XMRTO)
// xmr.to connects/widget
connect(ui->xmrToWidget, &XMRToWidget::viewOrder, m_ctx->XMRTo, &XmrTo::onViewOrder);
connect(ui->xmrToWidget, &XMRToWidget::getRates, m_ctx->XMRTo, &XmrTo::onGetRates);
connect(ui->xmrToWidget, &XMRToWidget::createOrder, m_ctx->XMRTo, &XmrTo::createOrder);
connect(m_ctx->XMRTo, &XmrTo::ratesUpdated, ui->xmrToWidget, &XMRToWidget::onRatesUpdated);
connect(m_ctx->XMRTo, &XmrTo::connectionError, ui->xmrToWidget, &XMRToWidget::onConnectionError);
connect(m_ctx->XMRTo, &XmrTo::connectionSuccess, ui->xmrToWidget, &XMRToWidget::onConnectionSuccess);
connect(m_ctx, &AppContext::balanceUpdated, ui->xmrToWidget, &XMRToWidget::onBalanceUpdated);
connect(m_ctx->XMRTo, &XmrTo::openURL, this, [=](const QString &url){ Utils::externalLinkWarning(this, url); });
connect(m_ctx, &AppContext::walletClosed, ui->xmrToWidget, &XMRToWidget::onWalletClosed);
ui->xmrToWidget->setHistoryModel(m_ctx->XMRTo->tableModel);
#else
ui->tabExchanges->setTabVisible(0, false);
#endif
#if defined(Q_OS_LINUX)
// system tray
m_trayIcon = new QSystemTrayIcon(QIcon(":/assets/images/appicons/64x64.png"));
@ -225,10 +209,6 @@ MainWindow::MainWindow(AppContext *ctx, QWidget *parent) :
connect(m_ctx, &AppContext::initiateTransaction, ui->sendWidget, &SendWidget::onInitiateTransaction);
connect(m_ctx, &AppContext::endTransaction, ui->sendWidget, &SendWidget::onEndTransaction);
// XMR.to
connect(m_ctx, &AppContext::initiateTransaction, ui->xmrToWidget, &XMRToWidget::onInitiateTransaction);
connect(m_ctx, &AppContext::endTransaction, ui->xmrToWidget, &XMRToWidget::onEndTransaction);
connect(m_ctx, &AppContext::initiateTransaction, [this]{
m_statusDots = 0;
m_constructingTransaction = true;
@ -421,13 +401,8 @@ void MainWindow::initMenu() {
m_tabShowHideMapper["Calc"] = new ToggleTab(ui->tabCalc, "Calc", "Calc", ui->actionShow_calc, Config::showTabCalc);
m_tabShowHideSignalMapper->setMapping(ui->actionShow_calc, "Calc");
#if defined(HAS_XMRTO)
connect(ui->actionShow_Exchange, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map));
m_tabShowHideMapper["Exchange"] = new ToggleTab(ui->tabExchange, "Exchange", "Exchange", ui->actionShow_Exchange, Config::showTabExchange);
m_tabShowHideSignalMapper->setMapping(ui->actionShow_Exchange, "Exchange");
#else
ui->actionShow_Exchange->setVisible(false);
#endif
ui->tabWidget->setTabVisible(Tabs::EXCHANGES, false);
#if defined(HAS_XMRIG)
connect(ui->actionShow_XMRig, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map));

39
src/mainwindow.ui

@ -293,39 +293,6 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTabWidget" name="tabExchanges">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabXMRto">
<attribute name="icon">
<iconset resource="assets.qrc">
<normaloff>:/assets/images/xmrto.png</normaloff>:/assets/images/xmrto.png</iconset>
</attribute>
<attribute name="title">
<string>XMR.to</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_10">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="XMRToWidget" name="xmrToWidget" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabXmrRig">
@ -764,12 +731,6 @@
<extends>QWidget</extends>
<header>historywidget.h</header>
</customwidget>
<customwidget>
<class>XMRToWidget</class>
<extends>QWidget</extends>
<header>xmrtowidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>CalcWidget</class>
<extends>QWidget</extends>

107
src/model/XmrToModel.cpp

@ -1,107 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#include "XmrToModel.h"
#include "model/ModelUtils.h"
#include "utils/xmrto.h"
#include "utils/ColorScheme.h"
XmrToModel::XmrToModel(QList<XmrToOrder*> *orders, QObject *parent)
: QAbstractTableModel(parent),
orders(orders)
{
}
void XmrToModel::update() {
beginResetModel();
endResetModel();
}
int XmrToModel::rowCount(const QModelIndex &) const {
return this->orders->count();
}
int XmrToModel::columnCount(const QModelIndex &) const {
return COUNT;
}
QVariant XmrToModel::data(const QModelIndex &index, int role) const {
const int _row = index.row();
const int _col = index.column();
const auto order = this->orders->at(_row);
if (role == Qt::DisplayRole){
switch(index.column()){
case Status:
{
QString status = XmrTo::stateMap[(OrderState) order->state];
if (order->state == OrderState::Status_OrderUnpaid)
return QString("%1 (%2)").arg(status, QString::number(order->countdown));
return status;
}
case ID:
return !order->uuid.isEmpty() ? order->uuid.split("-")[1] : "-";
case Destination:
return ModelUtils::displayAddress(order->btc_dest_address, 1);
case Conversion:
if(order->state <= OrderState::Status_OrderToBeCreated)
return "";
return QString("%1 XMR ⟶ %2 BTC").arg(QString::number(order->incoming_amount_total), QString::number(order->btc_amount));
case Rate:
return order->incoming_price_btc ? QString::number(order->incoming_price_btc, 'f', 6) : "";
case ErrorMsg:
if(order->errorMsg.isEmpty()) return "";
return order->errorMsg;
default: return {};
}
}
else if(role == Qt::BackgroundRole) {
if (_col == 0) {
if (order->state == OrderState::Status_OrderPaid || order->state == OrderState::Status_OrderPaidUnconfirmed)
return QBrush(ColorScheme::GREEN.asColor(true));
else if (order->state == OrderState::Status_OrderCreating || order->state == OrderState::Status_OrderToBeCreated)
return QBrush(ColorScheme::YELLOW.asColor(true));
else if (order->state == OrderState::Status_OrderUnpaid)
return QBrush(ColorScheme::YELLOW.asColor(true));
else if (order->state == OrderState::Status_OrderBTCSent)
return QBrush(ColorScheme::GREEN.asColor(true));
else if (order->state == OrderState::Status_OrderFailed || order->state == OrderState::Status_OrderTimedOut)
return QBrush(ColorScheme::RED.asColor(true));
}
}
else if (role == Qt::FontRole) {
switch(index.column()) {
case ID:
case Destination:
return ModelUtils::getMonospaceFont();
}
}
return QVariant();
}
QVariant XmrToModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
switch (section) {
case Status:
return QString("Status");
case ID:
return QString("ID");
case Destination:
return QString("Address");
case Conversion:
return QString("Conversion");
case Rate:
return QString("Rate");
case ErrorMsg:
return QString("Message");
default:
return QVariant();
}
}
return QVariant();
}

38
src/model/XmrToModel.h

@ -1,38 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#ifndef FEATHER_XMRTOMODEL_H
#define FEATHER_XMRTOMODEL_H
#include <QAbstractTableModel>
class XmrToOrder;
class XmrToModel : public QAbstractTableModel
{
Q_OBJECT
public:
enum ModelColumn
{
Status = 0,
ID,
Conversion,
Rate,
Destination,
ErrorMsg,
COUNT
};
XmrToModel(QList<XmrToOrder*> *orders, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QList<XmrToOrder*> *orders;
public slots:
void update();
};
#endif //FEATHER_XMRTOMODEL_H

110
src/utils/xmrto.cpp

@ -1,110 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#include "xmrto.h"
#include "appcontext.h"
QMap<OrderState, QString> XmrTo::stateMap;
XmrTo::XmrTo(AppContext *ctx, QObject *parent) :
QObject(parent),
m_ctx(ctx) {
m_baseUrl = m_ctx->networkType == NetworkType::Type::MAINNET ? "https://xmr.to" : "https://test.xmr.to";
m_netTor = new UtilsNetworking(this->m_ctx->network);
m_netClear = new UtilsNetworking(this->m_ctx->networkClearnet);
m_apiTor = new XmrToApi(this, m_netTor, m_baseUrl);
m_apiClear = new XmrToApi(this, m_netClear, m_baseUrl);
connect(m_apiTor, &XmrToApi::ApiResponse, this, &XmrTo::onApiResponse);
connect(m_apiClear, &XmrToApi::ApiResponse, this, &XmrTo::onApiResponse);
connect(this, &XmrTo::orderPaymentRequired, this->m_ctx, QOverload<XmrToOrder*>::of(&AppContext::onCreateTransaction));
XmrTo::stateMap[OrderState::Status_Idle] = "IDLE";
XmrTo::stateMap[OrderState::Status_OrderCreating] = "CREATING";
XmrTo::stateMap[OrderState::Status_OrderUnpaid] = "UNPAID";
XmrTo::stateMap[OrderState::Status_OrderToBeCreated] = "TO_BE_CREATED";
XmrTo::stateMap[OrderState::Status_OrderUnderPaid] = "UNDERPAID";
XmrTo::stateMap[OrderState::Status_OrderPaidUnconfirmed] = "PAID_UNCONFIRMED";
XmrTo::stateMap[OrderState::Status_OrderPaid] = "PAID";
XmrTo::stateMap[OrderState::Status_OrderBTCSent] = "BTC_SENT";
XmrTo::stateMap[OrderState::Status_OrderTimedOut] = "TIMED_OUT";
XmrTo::stateMap[OrderState::Status_OrderFailed] = "FAILED";
XmrTo::stateMap[OrderState::Status_OrderXMRSent] = "XMR_SENT";
this->tableModel = new XmrToModel(&this->orders, this);
}
void XmrTo::createOrder(double amount, const QString &currency, const QString &btcAddress) {
// ^[13][a-km-zA-HJ-NP-Z0-9]{26,33}$
XmrToOrder *order;
order = new XmrToOrder(this->m_ctx, m_netTor, m_baseUrl, false, &this->rates, this);
connect(order, &XmrToOrder::orderFailed, this, &XmrTo::orderFailed);
connect(order, &XmrToOrder::orderPaid, this, &XmrTo::orderPaid);
connect(order, &XmrToOrder::orderPaidUnconfirmed, this, &XmrTo::orderPaidUnconfirmed);
connect(order, &XmrToOrder::orderPaymentRequired, this, &XmrTo::orderPaymentRequired);
connect(order, &XmrToOrder::orderChanged, this->tableModel, &XmrToModel::update);
order->create(amount, currency, btcAddress);
this->orders.append(order);
tableModel->update();
}
void XmrTo::onApiResponse(const XmrToResponse &resp) {
if (!resp.ok) {
this->onApiFailure(resp);
return;
}
emit connectionSuccess();
if (resp.endpoint == Endpoint::RATES) {
onRatesReceived(resp.obj);
}
}
void XmrTo::onApiFailure(const XmrToResponse &resp) {
emit connectionError(resp.message);
}
void XmrTo::onGetRates() {
m_apiTor->getRates();
}
void XmrTo::onRatesReceived(const QJsonObject &doc) {
this->rates.price = doc.value("price").toString().toDouble();
this->rates.ln_lower_limit = doc.value("ln_lower_limit").toString().toDouble();
this->rates.ln_upper_limit = doc.value("ln_upper_limit").toString().toDouble();
this->rates.lower_limit = doc.value("lower_limit").toString().toDouble();
this->rates.upper_limit = doc.value("upper_limit").toString().toDouble();
this->rates.zero_conf_enabled = doc.value("zero_conf_enabled").toBool();
this->rates.zero_conf_max_amount = doc.value("zero_conf_enabled").toString().toDouble();
emit ratesUpdated(rates);
}
void XmrTo::onNetworkChanged(bool clearnet) {
m_api = clearnet ? m_apiClear : m_apiTor;
}
void XmrTo::onWalletClosed() {
// @TODO: cleanup
for(const auto &order: this->orders)
order->deleteLater();
this->tableModel->update();
}
void XmrTo::onWalletOpened() {
// @TODO: read past XMR.To orders, start pending ones
}
void XmrTo::onViewOrder(const QString &orderId) {
QString url = QString("%1/nojs/status/%2").arg(this->m_baseUrl).arg(orderId);
emit openURL(url);
}

63
src/utils/xmrto.h

@ -1,63 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#ifndef FEATHER_XMRTOCONVERT_H
#define FEATHER_XMRTOCONVERT_H
#include <QObject>
#include "model/XmrToModel.h"
#include "utils/xmrtoorder.h"
#include "utils/xmrtoapi.h"
class AppContext;
class XmrTo: public QObject {
Q_OBJECT
public:
explicit XmrTo(AppContext *ctx, QObject *parent = nullptr);
Q_ENUM(OrderState);
XmrToModel *tableModel;
static QMap<OrderState, QString> stateMap;
XmrToRates rates;
QList<XmrToOrder*> orders;
public slots:
void createOrder(double amount, const QString &currency, const QString &btcAddress);
void onGetRates();
void onRatesReceived(const QJsonObject &doc);
void onViewOrder(const QString &orderId);
void onNetworkChanged(bool clearnet);
void onWalletOpened();
void onWalletClosed();
private slots:
void onApiResponse(const XmrToResponse &doc);
signals:
void orderPaymentRequired(XmrToOrder *order);
void orderPaidUnconfirmed(XmrToOrder *order);
void orderPaid(XmrToOrder *order);
void orderFailed(XmrToOrder *order);
void ratesUpdated(XmrToRates rates);
void openURL(const QString &url);
void connectionError(const QString &err);
void connectionSuccess();
private:
void onApiFailure(const XmrToResponse &doc);
QString m_baseUrl;
AppContext *m_ctx;
int m_orderTimeout = 900; // https://xmrto-api.readthedocs.io/en/latest/introduction.html#various-parameters
UtilsNetworking *m_netTor;
UtilsNetworking *m_netClear;
XmrToApi *m_api;
XmrToApi *m_apiTor;
XmrToApi *m_apiClear;
};
#endif //FEATHER_XMRTOCONVERT_H

76
src/utils/xmrtoapi.cpp

@ -1,76 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#include "xmrtoapi.h"
XmrToApi::XmrToApi(QObject *parent, UtilsNetworking *network, QString baseUrl)
: QObject(parent)
, m_network(network)
, m_baseUrl(std::move(baseUrl))
{
}
void XmrToApi::getRates() {
QString url = QString("%1/api/v3/xmr2btc/order_parameter_query/").arg(this->m_baseUrl);
QNetworkReply *reply = m_network->getJson(url);
connect(reply, &QNetworkReply::finished, std::bind(&XmrToApi::onResponse, this, reply, Endpoint::RATES));
}
void XmrToApi::createOrder(double amount, const QString &amount_currency, const QString &dest_address) {
QJsonObject order;
order["amount"] = amount;
order["amount_currency"] = amount_currency;
order["btc_dest_address"] = dest_address;
QString url = QString("%1/api/v3/xmr2btc/order_create/").arg(m_baseUrl);
QNetworkReply *reply = m_network->postJson(url, order);
connect(reply, &QNetworkReply::finished, std::bind(&XmrToApi::onResponse, this, reply, Endpoint::ORDER_CREATE));
}
void XmrToApi::getOrderStatus(const QString &uuid) {
QJsonObject order;
order["uuid"] = uuid;
QString url = QString("%1/api/v3/xmr2btc/order_status_query/").arg(m_baseUrl);
QNetworkReply *reply = m_network->postJson(url, order);
connect(reply, &QNetworkReply::finished, std::bind(&XmrToApi::onResponse, this, reply, Endpoint::ORDER_STATUS));
}
void XmrToApi::onResponse(QNetworkReply *reply, Endpoint endpoint) {
const auto ok = reply->error() == QNetworkReply::NoError;
const auto err = reply->errorString();
QByteArray data = reply->readAll();
QJsonObject obj;
if (!data.isEmpty() && Utils::validateJSON(data)) {
auto doc = QJsonDocument::fromJson(data);
obj = doc.object();
}
else if (!ok) {
emit ApiResponse(XmrToResponse(false, endpoint, err));
return;
}
else {
emit ApiResponse(XmrToResponse(false, endpoint, "Invalid response from XMR.to"));
return;
}
XmrToError xmrto_err = XmrToApi::getApiError(obj);
if (!xmrto_err.code.isEmpty()) {
emit ApiResponse(XmrToResponse(false, endpoint, m_errorMap.contains(xmrto_err.code) ? m_errorMap[xmrto_err.code] : "", xmrto_err));
return;
}
reply->deleteLater();
emit ApiResponse(XmrToResponse(true, endpoint, "", obj));
}
XmrToError XmrToApi::getApiError(const QJsonObject &obj) {
if (!obj.contains("error"))
return XmrToError();
QString code = obj.value("error").toString();
QString msg = obj.value("error_msg").toString();
return XmrToError(code, msg);
}

87
src/utils/xmrtoapi.h

@ -1,87 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#ifndef FEATHER_XMRTOAPI_H
#define FEATHER_XMRTOAPI_H
#include <QObject>
#include <utility>
#include "utils/networking.h"
enum Endpoint {
RATES = 0,
ORDER_CREATE,
ORDER_STATUS
};
struct XmrToError {
explicit XmrToError(QString code = "", QString msg = "")
: code(std::move(code)), msg(std::move(msg)) {};
QString code;
QString msg;
};
struct XmrToResponse {
explicit XmrToResponse(bool ok, Endpoint endpoint, QString message, XmrToError error = XmrToError(), QJsonObject obj = {})
: ok(ok), endpoint(endpoint), message(std::move(message)), error(std::move(error)), obj(std::move(obj)) {};
explicit XmrToResponse(bool ok, Endpoint endpoint, QString message, QJsonObject obj)
: ok(ok), endpoint(endpoint), message(std::move(message)), obj(std::move(obj)) {};
bool ok;
Endpoint endpoint;
QString message;
XmrToError error = XmrToError();
QJsonObject obj;
};
class XmrToApi : public QObject {
Q_OBJECT
public:
explicit XmrToApi(QObject *parent, UtilsNetworking *network, QString baseUrl = "https://xmr.to");
void getRates();
void createOrder(double amount, const QString &amount_currency, const QString &dest_address);
void getOrderStatus(const QString &uuid);
signals:
void ApiResponse(XmrToResponse resp);
private slots:
void onResponse(QNetworkReply *reply, Endpoint endpoint);
private:
static XmrToError getApiError(const QJsonObject &obj);
QString m_baseUrl;
UtilsNetworking *m_network;
// https://xmrto-api.readthedocs.io/en/latest/introduction.html#list-of-all-error-codes
const QMap<QString, QString> m_errorMap = {
{"XMRTO-ERROR-001", "internal services not available, try again later."},
{"XMRTO-ERROR-002", "malformed bitcoin address, check address validity."},
{"XMRTO-ERROR-003", "invalid bitcoin amount, check amount data type."},
{"XMRTO-ERROR-004", "bitcoin amount out of bounds, check min and max amount."},
{"XMRTO-ERROR-005", "unexpected validation error, contact support."},
{"XMRTO-ERROR-006", "requested order not found, check order UUID."},
{"XMRTO-ERROR-007", "third party service not available, try again later."},
{"XMRTO-ERROR-008", "insufficient funds available, try again later."},
{"XMRTO-ERROR-009", "invalid request, check request parameters."},
{"XMRTO-ERROR-010", "payment protocol failed, invalid or outdated data served by URL."},
{"XMRTO-ERROR-011", "malformed payment protocol url, URL is malformed or cannot be contacted."},
{"XMRTO-ERROR-012", "too many requests, try less often."},
{"XMRTO-ERROR-013", "access forbidden."},
{"XMRTO-ERROR-014", "service is not available in your region."},
{"XMRTO-ERROR-015", "invalid monero amount, check amount data type."},
{"XMRTO-ERROR-016", "invalid currency, check available currency options."},
{"XMRTO-ERROR-017", "malformed lightning network invoice, provide a correct invoice for the main network."},
{"XMRTO-ERROR-018", "lightning payment unlikely to succeed, check first if xmr.to has routes available."},
{"XMRTO-ERROR-019", "lightning invoice preimage already known, don’t use the same invoice more than once."}
};
};
#endif //FEATHER_XMRTOAPI_H

274
src/utils/xmrtoorder.cpp

@ -1,274 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#include "xmrtoorder.h"
#include "libwalletqt/Wallet.h"
#include "appcontext.h"
#include "globals.h"
XmrToOrder::XmrToOrder(AppContext *ctx, UtilsNetworking *network, QString baseUrl, bool clearnet, XmrToRates *rates, QObject *parent) :
QObject(parent),
m_ctx(ctx),
m_network(network),
m_baseUrl(std::move(baseUrl)),
m_rates(rates),
m_clearnet(clearnet) {
this->state = OrderState::Status_Idle;
m_baseUrl = m_ctx->networkType == NetworkType::Type::MAINNET ? "https://xmr.to" : "https://test.xmr.to";
m_api = new XmrToApi(this, network, m_baseUrl);
connect(m_api, &XmrToApi::ApiResponse, this, &XmrToOrder::onApiResponse);
connect(m_ctx, &AppContext::transactionCommitted, this, &XmrToOrder::onTransactionCommitted);
connect(m_ctx, &AppContext::createTransactionCancelled, this, &XmrToOrder::onTransactionCancelled);
}
void XmrToOrder::onTransactionCancelled(const QVector<QString> &address, double amount) {
// listener for all cancelled transactions - will try to match the exact amount to this order.
if(this->incoming_amount_total != amount || this->receiving_subaddress != address[0]) return;
this->errorMsg = "TX cancelled by user";
this->changeState(OrderState::Status_OrderFailed);
this->stop();
}
void XmrToOrder::onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList& txid) {
// listener for all outgoing transactions - will try to match the exact amount to this order.
if(this->state == OrderState::Status_OrderUnpaid){
if(tx->amount() / globals::cdiv == this->incoming_amount_total) {
if(!status) {
this->errorMsg = "TX failed to commit";
this->changeState(OrderState::Status_OrderFailed);
this->stop();
return;
}
this->xmr_txid = txid.at(0);
this->m_paymentSent = true;
this->changeState(OrderState::Status_OrderXMRSent);
}
}
}
void XmrToOrder::onApiFailure(const XmrToResponse &resp) {
this->errorCode = resp.error.code;
this->errorMsg = resp.message;
switch (resp.endpoint) {
case ORDER_CREATE:
this->onCreatedError();
break;
case ORDER_STATUS:
this->onCheckedError(resp.error);
break;
default:
return;
}
}
void XmrToOrder::onApiResponse(const XmrToResponse& resp) {
if (!resp.ok) {
this->onApiFailure(resp);
return;
}
switch (resp.endpoint) {
case ORDER_CREATE:
this->onCreated(resp.obj);
break;
case ORDER_STATUS:
this->onChecked(resp.obj);
break;
default:
return;
}
}
void XmrToOrder::create(double amount, const QString &currency, const QString &btcAddress) {
if(this->m_ctx->currentWallet == nullptr) {
this->errorMsg = "No wallet opened";
this->changeState(OrderState::Status_OrderFailed);
return;
}
m_api->createOrder(amount, currency, btcAddress);
this->changeState(OrderState::Status_OrderCreating);
}
void XmrToOrder::onCreatedError() {
this->changeState(OrderState::Status_OrderFailed);
}
void XmrToOrder::onCreated(const QJsonObject &object) {
if(!object.contains("state"))
this->errorMsg = "Could not parse 'state' from JSON response";
if(object.value("state").toString() != "TO_BE_CREATED")
this->errorMsg = "unknown state from response, should be \"TO_BE_CREATED\"";
if(!this->errorMsg.isEmpty()) {
this->changeState(OrderState::Status_OrderFailed);
return;
}
if(m_created) return;
m_created = true;
this->btc_amount = object.value("btc_amount").toDouble();
this->btc_dest_address = object.value("btc_dest_address").toString();
this->uses_lightning = object.value("uses_lightning").toBool();
this->uuid = object.value("uuid").toString();
m_checkTimer.start(1000*5);
m_countdownTimer.start(1000);
connect(&m_checkTimer, &QTimer::timeout, this, &XmrToOrder::check);
connect(&m_countdownTimer, &QTimer::timeout, this, &XmrToOrder::onCountdown);
this->changeState(OrderState::Status_OrderToBeCreated);
this->check();
}
void XmrToOrder::check() {
if(this->m_ctx->currentWallet == nullptr)
return;
m_api->getOrderStatus(this->uuid);
}
void XmrToOrder::onCheckedError(const XmrToError& err) {
if (!err.code.isEmpty())
this->changeState(OrderState::Status_OrderFailed);
m_checkFailures += 1;
if(m_checkFailures > 15){
this->errorMsg = "Too many failed attempts";
this->changeState(OrderState::Status_OrderFailed);
}
}
void XmrToOrder::onChecked(const QJsonObject &object) {
if(object.contains("btc_amount"))
this->btc_amount = object.value("btc_amount").toString().toDouble();
if(object.contains("btc_dest_address"))
this->btc_dest_address = object.value("btc_dest_address").toString();
if(object.contains("seconds_till_timeout")) {
this->seconds_till_timeout = object.value("seconds_till_timeout").toInt();
this->countdown = this->seconds_till_timeout;
}
if(object.contains("created_at"))
this->created_at = object.value("created_at").toString();
if(object.contains("expires_at"))
this->expires_at = object.value("expires_at").toString();
if(object.contains("incoming_amount_total"))
this->incoming_amount_total = object.value("incoming_amount_total").toString().toDouble();
if(object.contains("remaining_amount_incoming"))
this->remaining_amount_incoming = object.value("remaining_amount_incoming").toString().toDouble();
if(object.contains("incoming_price_btc"))
{
qDebug() << object.value("incoming_price_btc").toString();
this->incoming_price_btc = object.value("incoming_price_btc").toString().toDouble();
}
if(object.contains("receiving_subaddress"))
this->receiving_subaddress = object.value("receiving_subaddress").toString();
if(object.contains("payments")) {
// detect btc txid, xmr.to api can output several - we'll just grab the first #yolo
auto payments = object.value("payments").toArray();
for(const auto &payment: payments){
auto obj = payment.toObject();
if(obj.contains("tx_id")) {
this->btc_txid = obj.value("tx_id").toString();
break;
}
}
}
this->changeState(object.value("state").toString());
}
void XmrToOrder::changeState(const QString &_state) {
for(const auto &key: XmrTo::stateMap.keys()) {
const auto &val = XmrTo::stateMap[key];
if(_state == val){
this->changeState(key);
return;
}
}
}
void XmrToOrder::changeState(OrderState _state) {
if(this->m_ctx->currentWallet == nullptr)
return;
if(_state == OrderState::Status_OrderUnderPaid && m_paymentSent) {
this->state = OrderState::Status_OrderXMRSent;
emit orderChanged();
return;
}
if(_state == this->state) return;
switch(_state){
case OrderState::Status_Idle:
break;
case OrderState::Status_OrderCreating:
break;
case OrderState::Status_OrderToBeCreated:
break;
case OrderState::Status_OrderUnderPaid:
emit orderFailed(this);
this->stop();
break;
case OrderState::Status_OrderUnpaid:
// need to send Monero
if(!m_paymentRequested) {
auto unlocked_balance = m_ctx->currentWallet->unlockedBalance() / globals::cdiv;
if (this->incoming_amount_total >= unlocked_balance) {
this->state = OrderState::Status_OrderFailed;
emit orderFailed(this);
this->stop();
break;
}
m_paymentRequested = true;
emit orderPaymentRequired(this);
}
break;
case OrderState::Status_OrderFailed:
emit orderFailed(this);
this->stop();
break;
case OrderState::Status_OrderPaidUnconfirmed:
emit orderPaidUnconfirmed(this);
break;
case OrderState::Status_OrderPaid:
emit orderPaid(this);
break;
case OrderState::Status_OrderTimedOut:
emit orderFailed(this);
this->stop();
break;
case OrderState::Status_OrderBTCSent:
emit orderPaid(this);
this->stop();
break;
default:
break;
}
this->state = _state;
emit orderChanged();
}
void XmrToOrder::onCountdown() {
if(this->countdown <= 0) return;
this->countdown -= 1;
emit orderChanged();
}
void XmrToOrder::stop(){
this->m_checkTimer.stop();
this->m_countdownTimer.stop();
}
XmrToOrder::~XmrToOrder(){
this->stop();
this->disconnect();
this->m_network->deleteLater();
}

106
src/utils/xmrtoorder.h

@ -1,106 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#ifndef FEATHER_XMRTOORDER_H
#define FEATHER_XMRTOORDER_H
#include <QObject>
#include "utils/networking.h"
#include "PendingTransaction.h"
#include "utils/xmrtoapi.h"
enum OrderState {
Status_Idle,
Status_OrderCreating,
Status_OrderToBeCreated,
Status_OrderUnpaid,
Status_OrderXMRSent,
Status_OrderUnderPaid,
Status_OrderPaidUnconfirmed,
Status_OrderPaid,
Status_OrderBTCSent,
Status_OrderTimedOut,
Status_OrderFailed
};
struct XmrToRates {
double price;
double upper_limit;
double lower_limit;
double ln_upper_limit;
double ln_lower_limit;
double zero_conf_max_amount;
bool zero_conf_enabled;
};
class XmrToOrder : public QObject {
Q_OBJECT
public:
explicit XmrToOrder(AppContext *ctx, UtilsNetworking *network, QString baseUrl, bool clearnet, XmrToRates *rates, QObject *parent = nullptr);
void create(double btcAmount, const QString &currency, const QString &btcAddress);
void changeState(OrderState state);
void changeState(const QString &state);
void stop();
int state;
int countdown = -1; // seconds remaining calculated from `seconds_till_timeout`
QString uuid;
QString errorMsg;
QString errorCode;
double btc_amount = 0;
QString btc_dest_address;
QString btc_txid;
QString xmr_txid;
bool uses_lightning = false;
QString receiving_subaddress;
QString created_at;
QString expires_at;
int seconds_till_timeout = -1;
double incoming_amount_total = 0; // amount_in_incoming_currency_for_this_order_as_string
double remaining_amount_incoming; // amount_in_incoming_currency_that_the_user_must_still_send_as_string
double incoming_price_btc = 0; // price_of_1_incoming_in_btc_currency_as_offered_by_service
public slots:
void onCountdown();
void onTransactionCancelled(const QVector<QString> &address, double amount);
void onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList& txid);
void onCreatedError();
void onChecked(const QJsonObject &object);
void onCheckedError(const XmrToError& err);
void check();
private:
bool m_created = false;
QString m_baseUrl;
QTimer m_checkTimer;
QTimer m_countdownTimer;
int m_checkFailures = 0;
bool m_clearnet;
bool m_paymentSent = false;
bool m_paymentRequested = false;
UtilsNetworking *m_network;
AppContext *m_ctx;
XmrToRates *m_rates;
XmrToApi *m_api;
~XmrToOrder();
signals:
void orderChanged();
void orderPaymentRequired(XmrToOrder *order);
void orderPaid(XmrToOrder *order);
void orderPaidUnconfirmed(XmrToOrder *order);
void orderFailed(XmrToOrder *order);
private slots:
void onCreated(const QJsonObject &object);
void onApiFailure(const XmrToResponse &resp);
void onApiResponse(const XmrToResponse &resp);
};
#endif //FEATHER_XMRTOORDER_H

193
src/xmrtowidget.cpp

@ -1,193 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#include "xmrtowidget.h"
#include "ui_xmrtowidget.h"
#include "dialog/xmrtoinfodialog.h"
#include "mainwindow.h"
#include "globals.h"
#include <QMessageBox>
XMRToWidget::XMRToWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::XMRToWidget)
{
ui->setupUi(this);
m_ctx = MainWindow::getContext();
QString amount_rx = R"(^\d*\.\d*$)";
QRegExp rx;
rx.setPattern(amount_rx);
QValidator *validator = new QRegExpValidator(rx, this);
ui->lineAmount->setValidator(validator);
// xmrto logo (c) binaryFate et. al. :-D
QPixmap p(":assets/images/xmrto_big.png");
ui->logo->setPixmap(p.scaled(112, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation));
ui->ratesLayout->hide();
// context menu
m_contextMenu = new QMenu();
m_showDetailsAction = m_contextMenu->addAction("Details");
m_viewOnXmrToAction = m_contextMenu->addAction("View order on XMR.to");
m_viewOnXmrToAction->setIcon(QIcon(":/assets/images/xmrto.png"));
connect(m_showDetailsAction, &QAction::triggered, this, &XMRToWidget::showInfoDialog);
connect(m_viewOnXmrToAction, &QAction::triggered