diff --git a/src/MorphTokenWidget.cpp b/src/MorphTokenWidget.cpp new file mode 100644 index 0000000..e0a849d --- /dev/null +++ b/src/MorphTokenWidget.cpp @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020, The Monero Project. + +#include "MorphTokenWidget.h" +#include "ui_MorphTokenWidget.h" +#include "mainwindow.h" + +#include + +MorphTokenWidget::MorphTokenWidget(QWidget *parent) : + QWidget(parent), + ui(new Ui::MorphTokenWidget) +{ + ui->setupUi(this); + m_ctx = MainWindow::getContext(); + + m_network = new UtilsNetworking(this->m_ctx->network); + m_api = new MorphTokenApi(this, m_network); + + connect(ui->btnCreateTrade, &QPushButton::clicked, this, &MorphTokenWidget::createTrade); + connect(ui->btn_lookupTrade, &QPushButton::clicked, this, &MorphTokenWidget::lookupTrade); + + connect(m_api, &MorphTokenApi::ApiResponse, this, &MorphTokenWidget::onApiResponse); + + connect(ui->combo_From, QOverload::of(&QComboBox::currentIndexChanged), [this](int index){ + ui->label_refundAddress->setText(QString("Refund address (%1):").arg(ui->combo_From->currentText())); + }); + connect(ui->combo_To, QOverload::of(&QComboBox::currentIndexChanged), [this](int index){ + ui->label_destinationAddress->setText(QString("Destination address (%1):").arg(ui->combo_To->currentText())); + }); + + connect(ui->check_autorefresh, &QCheckBox::toggled, [this](bool toggled){ + m_countdown = 30; + toggled ? m_countdownTimer.start(1000) : m_countdownTimer.stop(); + ui->check_autorefresh->setText("Autorefresh"); + }); + connect(&m_countdownTimer, &QTimer::timeout, this, &MorphTokenWidget::onCountdown); + + connect(ui->line_Id, &QLineEdit::textChanged, [this](const QString &text){ + ui->btn_lookupTrade->setEnabled(!text.isEmpty()); + ui->check_autorefresh->setEnabled(!text.isEmpty()); + }); + + // Default to BTC -> XMR + ui->combo_From->setCurrentIndex(1); + ui->combo_To->setCurrentIndex(0); + + ui->tabWidget->setTabVisible(2, false); +} + +void MorphTokenWidget::createTrade() { + QString inputAsset = ui->combo_From->currentText(); + QString outputAsset = ui->combo_To->currentText(); + QString refundAddress = ui->line_refundAddress->text(); + QString destinationAddress = ui->line_destinationAddress->text(); + + m_api->createTrade(inputAsset, outputAsset, refundAddress, destinationAddress); +} + +void MorphTokenWidget::lookupTrade() { + QString morphId = ui->line_Id->text(); + + if (!morphId.isEmpty()) + m_api->getTrade(morphId); +} + +void MorphTokenWidget::onApiResponse(const MorphTokenApi::MorphTokenResponse &resp) { + if (!resp.ok) { + ui->check_autorefresh->setChecked(false); + QMessageBox::warning(this, "MorphToken error", QString("Request failed:\n\n%1").arg(resp.message)); + return; + } + + ui->debugInfo->setPlainText(QJsonDocument(resp.obj).toJson(QJsonDocument::Indented)); + + if (resp.endpoint == MorphTokenApi::Endpoint::CREATE_TRADE || resp.endpoint == MorphTokenApi::Endpoint::GET_TRADE) { + ui->tabWidget->setCurrentIndex(1); + ui->line_Id->setText(resp.obj.value("id").toString()); + + auto obj = resp.obj; + auto input = obj["input"].toObject(); + auto output = obj["output"].toArray()[0].toObject(); + QString state = obj.value("state").toString(); + QString statusText; + + ui->trade->setTitle(QString("Trade (%1)").arg(state)); + + statusText += QString("Morph ID: %1\n\n").arg(obj["id"].toString()); + + if (state == "PENDING") { + statusText += QString("Waiting for a deposit, send %1 to %2\n").arg(input["asset"].toString(), + input["deposit_address"].toString()); + statusText += QString("Rate: 1 %1 -> %2 %3\n\n").arg(input["asset"].toString(), + output["seen_rate"].toString(), + output["asset"].toString()); + statusText += "Limits:\n"; + statusText += QString(" Minimum amount accepted: %1 %2\n").arg(formatAmount(input["asset"].toString(), input["limits"].toObject()["min"].toDouble()), + input["asset"].toString()); + statusText += QString(" Maximum amount accepted: %1 %2\n").arg(formatAmount(input["asset"].toString(), input["limits"].toObject()["max"].toDouble()), + input["asset"].toString()); + statusText += QString("\nSend a single deposit. If the amount is outside the limits, a refund will happen."); + } else if (state == "PROCESSING" || state == "TRADING" || state == "CONFIRMING") { + if (state == "CONFIRMING") { + statusText += QString("Waiting for confirmations\n"); + } else if (state == "TRADING") { + statusText += QString("Your transaction has been received and is confirmed. MorphToken is now executing your trade.\n" + "Usually this step takes no longer than a minute, " + "but in rare cases it can take a couple hours.\n" + "Wait a bit before contacting support.\n"); + } + statusText += QString("Converting %1 to %2\n").arg(input["asset"].toString(), output["asset"].toString()); + statusText += QString("Sending to %1\n").arg(output["address"].toString()); + statusText += QString("Stuck? Contact support at contact@morphtoken.com"); + } else if (state == "COMPLETE") { + if (output["txid"].toString().isEmpty()) { + statusText += QString("MorphToken is sending your transaction.\n"); + statusText += QString("MorphToken will send %1 %2 to %2").arg(this->formatAmount(output["asset"].toString(), output["converted_amount"].toDouble() - output["network_fee"].toObject()["fee"].toDouble()), + output["asset"].toString(), + output["address"].toString()); + } else { + statusText += QString("Sent %1 %2 to %3\ntxid: {}").arg(this->formatAmount(output["asset"].toString(), output["converted_amount"].toDouble() - output["network_fee"].toObject()["fee"].toDouble()), + output["asset"].toString(), + output["address"].toString(), + output["txid"].toString()); + } + } else if (state == "PROCESSING_REFUND" || state == "COMPLETE_WITH_REFUND") { + statusText += QString("MorphToken will refund %1 %2\nReason: %3\n").arg(obj["final_amount"].toString(), + obj["asset"].toString(), + obj["reason"].toString()); + + if (obj.contains("txid")) { + statusText += QString("txid: %1").arg(obj["txid"].toString()); + } + } else if (state == "COMPLETE_WITHOUT_REFUND") { + statusText += "Deposit amount below network fee, too small to refund."; + } + + ui->label_status->setText(statusText); + } + + if (resp.endpoint == MorphTokenApi::Endpoint::CREATE_TRADE) { + QMessageBox::information(this, "MorphToken", "Trade created!\n\nMake sure to save your Morph ID. You may need it in case something goes wrong."); + } +} + +void MorphTokenWidget::onCountdown() { + if (m_countdown > 0) { + m_countdown -= 1; + } else { + this->lookupTrade(); + m_countdown = 30; + } + ui->check_autorefresh->setText(QString("Autorefresh (%1)").arg(m_countdown)); +} + +QString MorphTokenWidget::formatAmount(const QString &asset, double amount) { + double displayAmount; + double div; + + if (asset == "ETH") + div = 1e18; + else if (asset == "XMR") + div = 1e12; + else + div = 1e8; + + displayAmount = amount / div; + + return QString::number(displayAmount, 'f', 8); +} + +MorphTokenWidget::~MorphTokenWidget() { + delete ui; +} diff --git a/src/MorphTokenWidget.h b/src/MorphTokenWidget.h new file mode 100644 index 0000000..99aa440 --- /dev/null +++ b/src/MorphTokenWidget.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020, The Monero Project. + +#ifndef FEATHER_MORPHTOKENWIDGET_H +#define FEATHER_MORPHTOKENWIDGET_H + +#include +#include "appcontext.h" +#include "utils/MorphTokenApi.h" + +namespace Ui { + class MorphTokenWidget; +} + +class MorphTokenWidget : public QWidget +{ +Q_OBJECT + +public: + explicit MorphTokenWidget(QWidget *parent = nullptr); + ~MorphTokenWidget() override; + +private: + void createTrade(); + void lookupTrade(); + void onApiResponse(const MorphTokenApi::MorphTokenResponse &resp); + + void onCountdown(); + + QString formatAmount(const QString &asset, double amount); + + Ui::MorphTokenWidget *ui; + + AppContext *m_ctx; + MorphTokenApi *m_api; + UtilsNetworking *m_network; + QTimer m_countdownTimer; + int m_countdown = 30; +}; + +#endif //FEATHER_MORPHTOKENWIDGET_H diff --git a/src/MorphTokenWidget.ui b/src/MorphTokenWidget.ui new file mode 100644 index 0000000..585c9f3 --- /dev/null +++ b/src/MorphTokenWidget.ui @@ -0,0 +1,348 @@ + + + MorphTokenWidget + + + + 0 + 0 + 1036 + 614 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Create trade + + + + + + + + + 0 + 0 + + + + From: + + + + + + + + XMR + + + + + BTC + + + + + ETH + + + + + BCH + + + + + LTC + + + + + DASH + + + + + + + + + 0 + 0 + + + + To: + + + + + + + + XMR + + + + + BTC + + + + + ETH + + + + + BCH + + + + + LTC + + + + + DASH + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Refund address (XMR): + + + + + + + + + + Destination address (XMR): + + + + + + + + + + + + + + false + + + Powered by MorphToken.com + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Create Trade + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Lookup trade + + + + + + Morph ID or MorphToken deposit address: + + + + + + + + + + + + false + + + Autorefresh + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Lookup trade + + + + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + Trade + + + + + + No trade loaded. + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Debug + + + + + + + 0 + 0 + + + + true + + + + + + + + + + + + diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 25a66d3..8622d7a 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -181,7 +181,7 @@ void AppContext::initTor() { this->tor = new Tor(this, this); this->tor->start(); - if (!(isTails || isWhonix)) { + if (!(isWhonix)) { auto networkProxy = new QNetworkProxy(QNetworkProxy::Socks5Proxy, Tor::torHost, Tor::torPort); this->network->setProxy(*networkProxy); if (m_wsUrl.host().endsWith(".onion")) diff --git a/src/assets.qrc b/src/assets.qrc index e699e14..69689a0 100644 --- a/src/assets.qrc +++ b/src/assets.qrc @@ -48,6 +48,7 @@ assets/images/lock_icon.png assets/images/lock.svg assets/images/microphone.png + assets/images/morphtoken.png assets/images/network.png assets/images/offline_tx.png assets/images/person.svg diff --git a/src/assets/images/morphtoken.png b/src/assets/images/morphtoken.png new file mode 100644 index 0000000..3d5db60 Binary files /dev/null and b/src/assets/images/morphtoken.png differ diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 52992a0..8ab6dd8 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -406,6 +406,10 @@ void MainWindow::initMenu() { m_tabShowHideMapper["Calc"] = new ToggleTab(ui->tabCalc, "Calc", "Calc", ui->actionShow_calc, Config::showTabCalc); m_tabShowHideSignalMapper->setMapping(ui->actionShow_calc, "Calc"); + connect(ui->actionShow_MorphToken, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); + m_tabShowHideMapper["MorphToken"] = new ToggleTab(ui->tabMorphToken, "MorphToken", "MorphToken", ui->actionShow_MorphToken, Config::showTabMorphToken); + m_tabShowHideSignalMapper->setMapping(ui->actionShow_MorphToken, "MorphToken"); + #if defined(XMRTO) connect(ui->actionShow_xmr_to, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); m_tabShowHideMapper["XMRto"] = new ToggleTab(ui->tabXmrTo, "XMRto", "XMR.to", ui->actionShow_xmr_to, Config::showTabXMRto); diff --git a/src/mainwindow.h b/src/mainwindow.h index c555bc2..774d588 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -76,6 +76,7 @@ public: RECEIVE, COINS, CALC, + MORPHTOKEN, XMR_TO, XMRIG }; diff --git a/src/mainwindow.ui b/src/mainwindow.ui index f9e2fc8..02a60be 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -6,7 +6,7 @@ 0 0 - 894 + 1156 496 @@ -238,6 +238,20 @@ + + + + :/assets/images/morphtoken.png:/assets/images/morphtoken.png + + + MorphToken + + + + + + + @@ -304,7 +318,7 @@ 0 0 - 894 + 1156 30 @@ -418,6 +432,7 @@ + @@ -670,6 +685,11 @@ Import transaction + + + Show MorphToken + + @@ -714,6 +734,12 @@
calcwidget.h
1 + + MorphTokenWidget + QWidget +
MorphTokenWidget.h
+ 1 +
diff --git a/src/utils/MorphTokenApi.cpp b/src/utils/MorphTokenApi.cpp new file mode 100644 index 0000000..eae1380 --- /dev/null +++ b/src/utils/MorphTokenApi.cpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020, The Monero Project. + +#include "MorphTokenApi.h" + +MorphTokenApi::MorphTokenApi(QObject *parent, UtilsNetworking *network, QString baseUrl) + : QObject(parent) + , m_network(network) + , m_baseUrl(std::move(baseUrl)) +{ +} + +void MorphTokenApi::createTrade(const QString &inputAsset, const QString &outputAsset, const QString &refundAddress, const QString &outputAddress) { + QJsonObject trade; + + QJsonObject input; + input["asset"] = inputAsset; + input["refund"] = refundAddress; + + QJsonArray output; + QJsonObject outputObj; + outputObj["asset"] = outputAsset; + outputObj["weight"] = 10000; + outputObj["address"] = outputAddress; + output.append(outputObj); + + trade["input"] = input; + trade["output"] = output; + + QString url = QString("%1/morph").arg(m_baseUrl); + QNetworkReply *reply = m_network->postJson(url, trade); + connect(reply, &QNetworkReply::finished, std::bind(&MorphTokenApi::onResponse, this, reply, Endpoint::CREATE_TRADE)); +} + +void MorphTokenApi::getTrade(const QString &morphId) { + QString url = QString("%1/morph/%2").arg(m_baseUrl, morphId); + QNetworkReply *reply = m_network->getJson(url); + connect(reply, &QNetworkReply::finished, std::bind(&MorphTokenApi::onResponse, this, reply, Endpoint::GET_TRADE)); +} + +void MorphTokenApi::getRates() { + QString url = QString("%1/rates").arg(m_baseUrl); + QNetworkReply *reply = m_network->getJson(url); + connect(reply, &QNetworkReply::finished, std::bind(&MorphTokenApi::onResponse, this, reply, Endpoint::GET_RATES)); +} + +void MorphTokenApi::getLimits(const QString &inputAsset, const QString &outputAsset) { + QJsonObject limits; + + QJsonObject input; + input["asset"] = inputAsset; + + QJsonArray output; + QJsonObject outputObj; + outputObj["asset"] = outputAsset; + outputObj["weight"] = 10000; + output.append(outputObj); + + limits["input"] = input; + limits["output"] = output; + + QString url = QString("%1/limits").arg(m_baseUrl); + QNetworkReply *reply = m_network->postJson(url, limits); + connect(reply, &QNetworkReply::finished, std::bind(&MorphTokenApi::onResponse, this, reply, Endpoint::GET_LIMITS)); +} + +void MorphTokenApi::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(MorphTokenResponse(false, endpoint, err, {})); + return; + } + else { + emit ApiResponse(MorphTokenResponse(false, endpoint, "Invalid response from MorphToken", {})); + return; + } + + if (obj.contains("success")) { + emit ApiResponse(MorphTokenResponse(false, endpoint, obj.value("description").toString(), obj)); + return; + } + + reply->deleteLater(); + emit ApiResponse(MorphTokenResponse(true, endpoint, "", obj)); +} \ No newline at end of file diff --git a/src/utils/MorphTokenApi.h b/src/utils/MorphTokenApi.h new file mode 100644 index 0000000..b96a96c --- /dev/null +++ b/src/utils/MorphTokenApi.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020, The Monero Project. + +#ifndef FEATHER_MORPHTOKENAPI_H +#define FEATHER_MORPHTOKENAPI_H + +#include +#include +#include "utils/networking.h" + +class MorphTokenApi : public QObject { + Q_OBJECT + +public: + enum Endpoint { + CREATE_TRADE = 0, + GET_TRADE, + GET_RATES, + GET_LIMITS + }; + + struct MorphTokenResponse { + explicit MorphTokenResponse(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; + QJsonObject obj; + }; + + explicit MorphTokenApi(QObject *parent, UtilsNetworking *network, QString baseUrl = "https://api.morphtoken.com"); + + void createTrade(const QString &inputAsset, const QString &outputAsset, const QString &refundAddress, const QString &outputAddress); + void getTrade(const QString &morphId); + void getRates(); + void getLimits(const QString &inputAsset, const QString &outputAsset); + +signals: + void ApiResponse(MorphTokenResponse resp); + +private slots: + void onResponse(QNetworkReply *reply, Endpoint endpoint); + +private: + QString m_baseUrl; + UtilsNetworking *m_network; +}; + + +#endif //FEATHER_MORPHTOKENAPI_H diff --git a/src/utils/config.cpp b/src/utils/config.cpp index a98bb2b..33eb201 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -41,6 +41,7 @@ static const QHash configStrings = { {Config::nodeSource,{QS("nodeSource"), 0}}, {Config::useOnionNodes,{QS("useOnionNodes"), false}}, {Config::showTabCoins,{QS("showTabCoins"), false}}, + {Config::showTabMorphToken, {QS("showTabMorphToken"), false}}, {Config::showTabXMRto,{QS("showTabXMRto"), true}}, {Config::showTabXMRig,{QS("showTabXMRig"), false}}, {Config::showTabCalc,{QS("showTabCalc"), true}}, diff --git a/src/utils/config.h b/src/utils/config.h index 759ee30..4e2a218 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -39,6 +39,7 @@ public: nodeSource, useOnionNodes, showTabCoins, + showTabMorphToken, showTabXMRto, showTabCalc, showTabXMRig,