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