You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wowlet/src/utils/xmrtoorder.cpp

274 lines
9.0 KiB

// 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 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) 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();
}