Version 0.9

DONE:
     1. Connect signals to make status of swap reflected in AtomicSwap dialog
     2. Add informational tabs to AtomicSwap dialog
     3. Add cancel and refund functionality to AtomicSwap when things go wrong
     4. Add recovery to atomic widget
     5. Refactor AtomicWidget so AtomicSwap handles parsing of swap binary output.
TODO:
     Test for bugs, make custom readme, build for other systems
This commit is contained in:
twiddle 2024-08-10 21:03:23 -04:00
parent 41f78b5f89
commit 12bbc76e26
8 changed files with 91 additions and 74 deletions

View file

@ -58,13 +58,14 @@ void AtomicConfigDialog::downloadBinary() {
tempFile = download->fileName(); tempFile = download->fileName();
QString url; QString url;
auto operatingSystem = Config::instance()->get(Config::operatingSystem).toString().toStdString(); auto operatingSystem = Config::instance()->get(Config::operatingSystem).toString().toStdString();
QString firstPart = "https://github.com/comit-network/xmr-btc-swap/releases/download/" + conf()->get(Config::swapVersion).toString();
if(strcmp("WIN",operatingSystem.c_str()) == 0) { if(strcmp("WIN",operatingSystem.c_str()) == 0) {
// HARD CODED DOWNload URL CHANGE IF PROBLEMS // HARD CODED DOWNload URL CHANGE IF PROBLEMS
url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.13.1/swap_0.13.1_Windows_x86_64.zip"); url = QString(firstPart+"/swap_"+conf()->get(Config::swapVersion).toString()+"_Windows_x86_64.zip");
} else if (strcmp("LINUX",operatingSystem.c_str())==0){ } else if (strcmp("LINUX",operatingSystem.c_str())==0){
url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.13.1/swap_0.13.1_Linux_x86_64.tar"); url = QString(firstPart+"/swap_"+conf()->get(Config::swapVersion).toString()+"_Linux_x86_64.tar");
} else { } else {
url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.13.1/swap_0.13.1_Linux_x86_64.tar"); url = QString(firstPart + "/swap_" + conf()->get(Config::swapVersion).toString() + "_Darwin_x86_64.tar");
} }
archive = network->get(this, url); archive = network->get(this, url);
@ -72,7 +73,6 @@ void AtomicConfigDialog::downloadBinary() {
QStringList answer; QStringList answer;
connect(archive,&QNetworkReply::readyRead, this, [this]{ connect(archive,&QNetworkReply::readyRead, this, [this]{
QByteArray data= archive->readAll(); QByteArray data= archive->readAll();
qDebug() << "received data of size: " << data.size();
download->write(data.constData(), data.size()); download->write(data.constData(), data.size());
}); });
connect(archive, &QNetworkReply::finished, connect(archive, &QNetworkReply::finished,
@ -82,7 +82,6 @@ void AtomicConfigDialog::downloadBinary() {
void AtomicConfigDialog::extract() { void AtomicConfigDialog::extract() {
ui->downloadLabel->setText("Download Successful, extracting binary to final destination"); ui->downloadLabel->setText("Download Successful, extracting binary to final destination");
qDebug() << "extracting";
download->close(); download->close();
archive->deleteLater(); archive->deleteLater();

View file

@ -9,6 +9,7 @@
#include "History.h" #include "History.h"
#include "config.h" #include "config.h"
#include "AtomicSwap.h" #include "AtomicSwap.h"
#include "Utils.h"
#include <QStandardItemModel> #include <QStandardItemModel>
AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) :
@ -57,8 +58,20 @@ AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) :
} }
arguments << "--swap-id"; arguments << "--swap-id";
arguments << ui->swap_history->selectionModel()->selectedRows().at(0).sibling(0,1).data().toString(); arguments << ui->swap_history->selectionModel()->selectedRows().at(0).sibling(0,1).data().toString();
arguments << "--tor-socks5-port"; arguments << "--monero-daemon-address";
arguments << conf()->get(Config::socks5Port).toString(); auto nodes = conf()->get(Config::nodes).toJsonObject();
if (nodes.isEmpty()) {
auto jsonData = conf()->get(Config::nodes).toByteArray();
if (Utils::validateJSON(jsonData)) {
auto doc = QJsonDocument::fromJson(jsonData);
nodes = doc.object();
}
}
arguments << nodes.value("0").toObject()["ws"].toArray()[0].toString();
if(conf()->get(Config::proxy).toInt() != Config::Proxy::None) {
arguments << "--tor-socks5-port";
arguments << conf()->get(Config::socks5Port).toString();
}
swapDialog->runSwap(arguments); swapDialog->runSwap(arguments);
}); });
} }

View file

@ -6,8 +6,8 @@
#include "AtomicSwap.h" #include "AtomicSwap.h"
#include <utility>
#include <QJsonParseError> #include <QJsonParseError>
#include <qt6/QtWidgets/QMessageBox>
#include "ui_AtomicSwap.h" #include "ui_AtomicSwap.h"
#include "AtomicWidget.h" #include "AtomicWidget.h"
@ -15,7 +15,6 @@
AtomicSwap::AtomicSwap(QWidget *parent) : AtomicSwap::AtomicSwap(QWidget *parent) :
WindowModalDialog(parent), ui(new Ui::AtomicSwap), fundDialog( new AtomicFundDialog(this)), procList(new QList<QSharedPointer<QProcess>>()) { WindowModalDialog(parent), ui(new Ui::AtomicSwap), fundDialog( new AtomicFundDialog(this)), procList(new QList<QSharedPointer<QProcess>>()) {
ui->setupUi(this); ui->setupUi(this);
//ui->debug_log->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
ui->label_status->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); ui->label_status->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse);
QPixmap pixmapTarget = QPixmap(":/assets/images/hint-icon.png"); QPixmap pixmapTarget = QPixmap(":/assets/images/hint-icon.png");
int size=20; int size=20;
@ -33,7 +32,6 @@ void AtomicSwap::runSwap(QStringList arguments){
swap->setProcessChannelMode(QProcess::MergedChannels); swap->setProcessChannelMode(QProcess::MergedChannels);
swap->setReadChannel(QProcess::StandardOutput); swap->setReadChannel(QProcess::StandardOutput);
connect(swap, &QProcess::readyRead,this, [this, swap] { connect(swap, &QProcess::readyRead,this, [this, swap] {
//Refactor and move this to a slot in atomicswap, move fund dialog to be part of atomic swap
while(swap->canReadLine()){ while(swap->canReadLine()){
QJsonParseError err; QJsonParseError err;
const QByteArray& rawline = swap->readLine(); const QByteArray& rawline = swap->readLine();
@ -51,14 +49,18 @@ void AtomicSwap::runSwap(QStringList arguments){
fundDialog->show(); fundDialog->show();
} else if (line["fields"]["message"].toString().startsWith("Received Bitcoin")){ } else if (line["fields"]["message"].toString().startsWith("Received Bitcoin")){
this->updateStatus(line["fields"]["new_balance"].toString().split(" ")[0] + " BTC received, starting swap"); this->updateStatus(line["fields"]["new_balance"].toString().split(" ")[0] + " BTC received, starting swap");
this->setSwap(line["span"]["swap_id"].toString()); QVariant var;
var.setValue(HistoryEntry {QDateTime::currentDateTime(),line["span"]["swap_id"].toString()});
QVariantList past = conf()->get(Config::pendingSwap).toList();
past.append(var);
conf()->set(Config::pendingSwap,past);
fundDialog->close(); fundDialog->close();
qDebug() << "Spawn atomic swap progress dialog"; qDebug() << "Spawn atomic swap progress dialog";
this->show(); this->show();
} else if ( QString confs = line["fields"]["seen_confirmations"].toString(); !confs.isEmpty()){ } else if ( QString confs = line["fields"]["seen_confirmations"].toString(); !confs.isEmpty()){
qDebug() << "Updating xmrconfs " + confs; qDebug() << "Updating xmrconfs " + confs;
this->updateXMRConf(confs.toInt()); this->updateXMRConf(confs.toInt());
} else if (QString message = line["fields"]["message"].toString(); !QString::compare(message, "Bitcoin transaction status changed")){ } else if (QString message = line["fields"]["message"].toString(); QString::compare(message, "Bitcoin transaction status changed")==0){
qDebug() << "Updating btconfs " + line["fields"]["new_status"].toString().split(" ")[2]; qDebug() << "Updating btconfs " + line["fields"]["new_status"].toString().split(" ")[2];
QString status = line["fields"]["new_status"].toString(); QString status = line["fields"]["new_status"].toString();
bool ok; bool ok;
@ -68,9 +70,31 @@ void AtomicSwap::runSwap(QStringList arguments){
} else { } else {
this->updateStatus("Found txid " + line["fields"]["txid"].toString() + " in mempool"); this->updateStatus("Found txid " + line["fields"]["txid"].toString() + " in mempool");
} }
} else if (QString message = line["fields"]["message"].toString(); message.startsWith("Swap completed")){
QVariantList past = conf()->get(Config::pendingSwap).toList();
past.removeLast();
conf()->set(Config::pendingSwap, past);
this->updateStatus("Swap has successfully completed you can close this window now");
} else if (QString message = line["fields"]["message"].toString(); QString::compare(message,"Advancing state")==0){
this->updateStatus("State of swap has advanced to " + line["fields"]["state"].toString());
} else if (QString refund = line["fields"]["kind"].toString(); QString::compare(refund,"refund")==0){
QString txid = line["fields"]["txid"].toString();
QString id = line["span"]["swap_id"].toString();
QVariantList past = conf()->get(Config::pendingSwap).toList();
for(int i=0;i<past.length();i++){
if(QString::compare(past[i].value<HistoryEntry>().id,id)==0) {
past.remove(i);
break;
}
}
conf()->set(Config::pendingSwap, past);
QMessageBox::information(this,"Cancel and Refund","Swap refunded succesfully with txid " + txid);
} else if (QString message = line["fields"]["message"].toString(); QString::compare(message, "API call resulted in an error")==0){
QString err = line["fields"]["err"].toString().split("\n")[0].split(":")[1];
QMessageBox::warning(this, "Cancel and Refund", "Time lock hasn't expired yet so cancel failed. Try again in " + err + "blocks");
} else if (QString message = line["fields"]["latest_version"].toString(); !message.isEmpty()){
conf()->set(Config::swapVersion,message);
} }
//Insert line conditionals here
} }
}); });
@ -82,18 +106,8 @@ AtomicSwap::~AtomicSwap() {
for (const auto& proc : *procList){ for (const auto& proc : *procList){
proc->kill(); proc->kill();
} }
if(QString::compare("WINDOWS",conf()->get(Config::operatingSystem).toString()) != 0) {
qDebug() << "Closing monero-wallet-rpc";
(new QProcess)->start("kill", QStringList{"-f", Config::defaultConfigDir().absolutePath() +
"/mainnet/monero/monero-wallet-rpc"});
(new QProcess)->start("kill", QStringList{"-f", Config::defaultConfigDir().absolutePath() +
"/testnet/monero/monero-wallet-rpc"});
}
} }
void AtomicSwap::logLine(QString line){ void AtomicSwap::logLine(QString line){
//ui->debug_log->setText(ui->debug_log->toPlainText().append(QTime::currentTime().toString() + ":" + line));
this->update(); this->update();
} }
void AtomicSwap::updateStatus(QString status){ void AtomicSwap::updateStatus(QString status){
@ -117,9 +131,6 @@ void AtomicSwap::setTitle(QString title) {
this->update(); this->update();
} }
void AtomicSwap::setSwap(QString swapId){
id = std::move(swapId);
}
void AtomicSwap::cancel(){ void AtomicSwap::cancel(){

View file

@ -27,7 +27,6 @@ public:
void updateBTCConf(int confs); void updateBTCConf(int confs);
void updateXMRConf(int confs); void updateXMRConf(int confs);
void setTitle(QString title); void setTitle(QString title);
void setSwap(QString swapId);
public slots: public slots:
void runSwap(QStringList swap); void runSwap(QStringList swap);
signals: signals:

View file

@ -61,19 +61,45 @@ AtomicWidget::AtomicWidget(QWidget *parent)
connect(ui->btn_swap, &QPushButton::clicked, this, [this]{ connect(ui->btn_swap, &QPushButton::clicked, this, [this]{
auto rows = ui->offerBookTable->selectionModel()->selectedRows(); auto rows = ui->offerBookTable->selectionModel()->selectedRows();
clean(); clean();
// UNCOMENT after testing if (rows.size() < 1){
//if (rows.size() < 1){ ui->meta_label->setText("You must select an offer to use for swap, refresh if there aren't any");
// ui->meta_label->setText("You must select an offer to use for swap, refresh if there aren't any"); } else {
//} else { QModelIndex index = rows.at(0);
//QModelIndex index = rows.at(0); QString seller = index.sibling(index.row(), 3).data().toString();
//QString seller = index.sibling(index.row(), 3).data().toString();
QString seller = "test";
//Add proper error checking on ui input after rest of swap is implemented //Add proper error checking on ui input after rest of swap is implemented
QString btcChange = ui->change_address->text(); QString btcChange = ui->change_address->text();
QRegularExpression btcMain("^(bc1)[a-zA-HJ-NP-Z0-9]{39}$");
QRegularExpression btcTest("^(tb1)[a-zA-HJ-NP-Z0-9]{39}$");
QString xmrReceive = ui->xmr_address->text(); QString xmrReceive = ui->xmr_address->text();
if(xmrReceive.isEmpty()) {
QMessageBox::warning(this, "Warning", "XMR receive address is required to start swap");
return;
}
QRegularExpression xmrMain("^[48][0-9AB][1-9A-HJ-NP-Za-km-z]{93}");
QRegularExpression xmrStage("^[57][0-9AB][1-9A-HJ-NP-Za-km-z]{93}");
if (constants::networkType==NetworkType::STAGENET){
if(!btcChange.isEmpty() && !btcTest.match(btcChange).hasMatch()){
QMessageBox::warning(this, "Warning","BTC change address is wrong, not a bech32 segwit address, or on wrong network");
return;
}
if(!xmrStage.match(xmrReceive).hasMatch()){
QMessageBox::warning(this, "Warning","XMR receive address is improperly formated or on wrong network");
return;
}
} else {
if(!btcChange.isEmpty() && !btcMain.match(btcChange).hasMatch()){
QMessageBox::warning(this, "Warning","BTC change address is wrong, not a bech32 segwit address,or on wrong network");
return;
}
if(!xmrMain.match(xmrReceive).hasMatch()){
QMessageBox::warning(this, "Warning","XMR receive address is improperly formated or on wrong network");
return;
}
}
sleep(1); sleep(1);
runSwap(seller,btcChange, xmrReceive); runSwap(seller,btcChange, xmrReceive);
//} }
}); });
connect(ui->btn_addRendezvous, &QPushButton::clicked, this, [this]{ connect(ui->btn_addRendezvous, &QPushButton::clicked, this, [this]{
@ -89,11 +115,6 @@ AtomicWidget::AtomicWidget(QWidget *parent)
} }
}); });
//Remove after testing
QVariant var;
var.setValue(HistoryEntry {QDateTime::currentDateTime(),"test-id"});
conf()->set(Config::pendingSwap, QVariantList{var});
auto recd = new AtomicRecoverDialog(this); auto recd = new AtomicRecoverDialog(this);
if (!recd->historyEmpty()){ if (!recd->historyEmpty()){
recd->show(); recd->show();
@ -124,18 +145,10 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons
arguments << "-j"; arguments << "-j";
arguments << "buy-xmr"; arguments << "buy-xmr";
arguments << "--change-address"; arguments << "--change-address";
arguments << "tb1qzndh6u8qgl2ee4k4gl9erg947g67hyx03vvgen"; arguments << btcChange;
//arguments << btcChange;
arguments << "--receive-address"; arguments << "--receive-address";
arguments << "78YnzFTp3UUMgtKuAJCP2STcbxRZPDPveJ5YGgfg5doiPahS9suWF1r3JhKqjM1McYBJvu8nhkXExGfXVkU6n5S6AXrg4KP"; arguments << xmrReceive;
//arguments << xmrReceive;
arguments << "--seller";
arguments << "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooW9yDFYojXnZRdqS9UXcfP2amgwoYdSjujwWdRw4LTSdWw";
// Remove after testing
arguments << "--electrum-rpc";
arguments << "tcp://127.0.0.1:50001";
arguments << "--bitcoin-target-block";
arguments << "1";
auto nodes = conf()->get(Config::nodes).toJsonObject(); auto nodes = conf()->get(Config::nodes).toJsonObject();
if (nodes.isEmpty()) { if (nodes.isEmpty()) {
auto jsonData = conf()->get(Config::nodes).toByteArray(); auto jsonData = conf()->get(Config::nodes).toByteArray();
@ -144,12 +157,10 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons
nodes = doc.object(); nodes = doc.object();
} }
} }
qDebug() << nodes.value("0").toObject()["ws"].toArray()[0];
arguments << "--monero-daemon-address"; arguments << "--monero-daemon-address";
//arguments << "node.monerodevs.org:38089";
arguments << nodes.value("0").toObject()["ws"].toArray()[0].toString(); arguments << nodes.value("0").toObject()["ws"].toArray()[0].toString();
// Uncomment after testing arguments << "--seller";
//arguments << seller; arguments << seller;
if(conf()->get(Config::proxy).toInt() != Config::Proxy::None) { if(conf()->get(Config::proxy).toInt() != Config::Proxy::None) {
arguments << "--tor-socks5-port"; arguments << "--tor-socks5-port";
arguments << conf()->get(Config::socks5Port).toString(); arguments << conf()->get(Config::socks5Port).toString();
@ -176,23 +187,16 @@ void AtomicWidget::list(const QString& rendezvous) {
auto *swap = new QProcess(); auto *swap = new QProcess();
procList->append(QSharedPointer<QProcess>(swap)); procList->append(QSharedPointer<QProcess>(swap));
swap->setReadChannel(QProcess::StandardError); swap->setReadChannel(QProcess::StandardError);
//swap->start(conf()->get(Config::swapPath).toString(), arguments);
connect(swap, &QProcess::finished, this, [this, swap]{ connect(swap, &QProcess::finished, this, [this, swap]{
QJsonDocument parsedLine; QJsonDocument parsedLine;
QJsonParseError parseError; QJsonParseError parseError;
QList<QSharedPointer<OfferEntry>> list; QList<QSharedPointer<OfferEntry>> list;
qDebug() << "Subprocess has finished";
auto output = QString::fromLocal8Bit(swap->readAllStandardError()); auto output = QString::fromLocal8Bit(swap->readAllStandardError());
qDebug() << "Crashes before splitting";
auto lines = output.split(QRegularExpression("[\r\n]"),Qt::SkipEmptyParts); auto lines = output.split(QRegularExpression("[\r\n]"),Qt::SkipEmptyParts);
qDebug() << lines.size();
qDebug() << "parsing Output";
for(const auto& line : lines){ for(const auto& line : lines){
qDebug() << line;
if(line.contains("status")){ if(line.contains("status")){
qDebug() << "status contained";
parsedLine = QJsonDocument::fromJson(line.toLocal8Bit(), &parseError ); parsedLine = QJsonDocument::fromJson(line.toLocal8Bit(), &parseError );
if (parsedLine["fields"]["status"].toString().contains("Online")){ if (parsedLine["fields"]["status"].toString().contains("Online")){
bool skip = false; bool skip = false;
@ -205,17 +209,14 @@ void AtomicWidget::list(const QString& rendezvous) {
ui->meta_label->setText("Updated offer book"); ui->meta_label->setText("Updated offer book");
offerList->append(QSharedPointer<OfferEntry>(entry)); offerList->append(QSharedPointer<OfferEntry>(entry));
} }
qDebug() << entry;
} }
} }
} }
qDebug() << "exits fine";
swap->close(); swap->close();
o_model->updateOffers(*offerList); o_model->updateOffers(*offerList);
return list; return list;
}); });
swap->start(conf()->get(Config::swapPath).toString(), arguments); swap->start(conf()->get(Config::swapPath).toString(), arguments);
//swap->waitForFinished(120000);
@ -234,13 +235,6 @@ void AtomicWidget::clean() {
for (const auto& proc : *procList){ for (const auto& proc : *procList){
proc->kill(); proc->kill();
} }
if(QString::compare("WINDOWS",conf()->get(Config::operatingSystem).toString()) != 0) {
qDebug() << "Closing monero-wallet-rpc";
(new QProcess)->start("kill", QStringList{"-f", Config::defaultConfigDir().absolutePath() +
"/mainnet/monero/monero-wallet-rpc"});
(new QProcess)->start("kill", QStringList{"-f", Config::defaultConfigDir().absolutePath() +
"/testnet/monero/monero-wallet-rpc"});
}
} }

View file

@ -21,7 +21,6 @@ void OfferModel::clear() {
void OfferModel::updateOffers(const QList<QSharedPointer<OfferEntry>> &posts) { void OfferModel::updateOffers(const QList<QSharedPointer<OfferEntry>> &posts) {
beginResetModel(); beginResetModel();
qDebug() << "updating Offers";
m_offers.clear(); m_offers.clear();
for (const auto& post : posts) { for (const auto& post : posts) {
m_offers.push_back(post); m_offers.push_back(post);

View file

@ -150,6 +150,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::swapPath, {QS("swapPath"), ""}}, {Config::swapPath, {QS("swapPath"), ""}},
{Config::operatingSystem, {QS("operatingSystem"), OS}}, {Config::operatingSystem, {QS("operatingSystem"), OS}},
{Config::pendingSwap, {QS("pendingSwap"), QVariantList{}}}, {Config::pendingSwap, {QS("pendingSwap"), QVariantList{}}},
{Config::swapVersion, {QS("swapVersion"), "0.13.4"}},
}; };

View file

@ -156,6 +156,7 @@ public:
swapPath, swapPath,
operatingSystem, operatingSystem,
pendingSwap, pendingSwap,
swapVersion,
}; };
enum PrivacyLevel { enum PrivacyLevel {