From 140547b38e4135c7d0dbdefeff9cd3af7fc51871 Mon Sep 17 00:00:00 2001 From: twiddle Date: Sat, 18 May 2024 12:49:03 -0400 Subject: [PATCH 01/26] Plugin Tab placeholder copy of Calc --- CMakeLists.txt | 1 + src/CMakeLists.txt | 2 +- src/plugins/atomic/AtomicConfigDialog.cpp | 101 ++++++++++++ src/plugins/atomic/AtomicConfigDialog.h | 41 +++++ src/plugins/atomic/AtomicConfigDialog.ui | 153 ++++++++++++++++++ src/plugins/atomic/AtomicPlugin.cpp | 70 ++++++++ src/plugins/atomic/AtomicPlugin.h | 41 +++++ src/plugins/atomic/AtomicWidget.cpp | 170 ++++++++++++++++++++ src/plugins/atomic/AtomicWidget.h | 41 +++++ src/plugins/atomic/AtomicWidget.ui | 187 ++++++++++++++++++++++ src/plugins/atomic/AtomicWindow.cpp | 21 +++ src/plugins/atomic/AtomicWindow.h | 26 +++ src/plugins/atomic/AtomicWindow.ui | 49 ++++++ src/utils/config.cpp | 4 +- src/wizard/WalletWizard.cpp | 2 + src/wizard/WalletWizard.h | 3 +- 16 files changed, 908 insertions(+), 4 deletions(-) create mode 100644 src/plugins/atomic/AtomicConfigDialog.cpp create mode 100644 src/plugins/atomic/AtomicConfigDialog.h create mode 100644 src/plugins/atomic/AtomicConfigDialog.ui create mode 100644 src/plugins/atomic/AtomicPlugin.cpp create mode 100644 src/plugins/atomic/AtomicPlugin.h create mode 100644 src/plugins/atomic/AtomicWidget.cpp create mode 100644 src/plugins/atomic/AtomicWidget.h create mode 100644 src/plugins/atomic/AtomicWidget.ui create mode 100644 src/plugins/atomic/AtomicWindow.cpp create mode 100644 src/plugins/atomic/AtomicWindow.h create mode 100644 src/plugins/atomic/AtomicWindow.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 6023cfde..fa962f6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ option(WITH_PLUGIN_BOUNTIES "Include Bounties Home plugin" ON) option(WITH_PLUGIN_REVUO "Include Revuo Home plugin" ON) option(WITH_PLUGIN_CALC "Include Calc tab plugin" ON) option(WITH_PLUGIN_XMRIG "Include XMRig plugin" ON) +option(WITH_PLUGIN_ATOMIC "Include Atomic plugin" ON) list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_SOURCE_DIR}/cmake") include(CheckCCompilerFlag) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ae06ddb8..49384469 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -225,7 +225,7 @@ if(STATIC) endif() if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") - target_compile_definitions(feather PRIVATE QT_NO_DEBUG=1) + target_compile_definitions(feather PRIVATE QT_NO_DEBUG=0) endif() target_compile_definitions(feather diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp new file mode 100644 index 00000000..1cd7d53c --- /dev/null +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "AtomicConfigDialog.h" +#include "ui_AtomicConfigDialog.h" + +#include "utils/AppData.h" +#include "utils/config.h" + +AtomicConfigDialog::AtomicConfigDialog(QWidget *parent) + : WindowModalDialog(parent) + , ui(new Ui::AtomicConfigDialog) +{ + ui->setupUi(this); + + this->fillListWidgets(); + + connect(ui->btn_selectAll, &QPushButton::clicked, this, &AtomicConfigDialog::selectAll); + connect(ui->btn_deselectAll, &QPushButton::clicked, this, &AtomicConfigDialog::deselectAll); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, [this]{ + conf()->set(Config::fiatSymbols, this->checkedFiat()); + conf()->set(Config::cryptoSymbols, this->checkedCrypto()); + this->accept(); + }); + + this->adjustSize(); +} + +QStringList AtomicConfigDialog::checkedFiat() { + return this->getChecked(ui->list_fiat); +} + +QStringList AtomicConfigDialog::checkedCrypto() { + return this->getChecked(ui->list_crypto); +} + +void AtomicConfigDialog::selectAll() { + this->setCheckState(this->getVisibleListWidget(), Qt::Checked); +} + +void AtomicConfigDialog::deselectAll() { + this->setCheckState(this->getVisibleListWidget(), Qt::Unchecked); +} + +void AtomicConfigDialog::setCheckState(QListWidget *widget, Qt::CheckState checkState) { + QListWidgetItem *item; + for (int i=0; i < widget->count(); i++) { + item = widget->item(i); + item->setCheckState(checkState); + } +} + +QStringList AtomicConfigDialog::getChecked(QListWidget *widget) { + QStringList checked; + QListWidgetItem *item; + for (int i=0; i < widget->count(); i++) { + item = widget->item(i); + if (item->checkState() == Qt::Checked) { + checked.append(item->text()); + } + } + return checked; +} + +QListWidget* AtomicConfigDialog::getVisibleListWidget() { + if (ui->tabWidget->currentIndex() == 0) { + return ui->list_fiat; + } else { + return ui->list_crypto; + } +} + +void AtomicConfigDialog::fillListWidgets() { + QStringList cryptoCurrencies = appData()->prices.markets.keys(); + QStringList fiatCurrencies = appData()->prices.rates.keys(); + + QStringList checkedCryptoCurrencies = conf()->get(Config::cryptoSymbols).toStringList(); + QStringList checkedFiatCurrencies = conf()->get(Config::fiatSymbols).toStringList(); + + ui->list_crypto->addItems(cryptoCurrencies); + ui->list_fiat->addItems(fiatCurrencies); + + auto setChecked = [](QListWidget *widget, const QStringList &checked){ + QListWidgetItem *item; + for (int i=0; i < widget->count(); i++) { + item = widget->item(i); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + if (checked.contains(item->text())) { + item->setCheckState(Qt::Checked); + } else { + item->setCheckState(Qt::Unchecked); + } + } + }; + + setChecked(ui->list_crypto, checkedCryptoCurrencies); + setChecked(ui->list_fiat, checkedFiatCurrencies); +} + +AtomicConfigDialog::~AtomicConfigDialog() = default; \ No newline at end of file diff --git a/src/plugins/atomic/AtomicConfigDialog.h b/src/plugins/atomic/AtomicConfigDialog.h new file mode 100644 index 00000000..a51d28d4 --- /dev/null +++ b/src/plugins/atomic/AtomicConfigDialog.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_ATOMICCONFIGDIALOG_H +#define FEATHER_ATOMICCONFIGDIALOG_H + +#include +#include + +#include "components.h" + +namespace Ui { + class AtomicConfigDialog; +} + +class AtomicConfigDialog : public WindowModalDialog +{ +Q_OBJECT + +public: + explicit AtomicConfigDialog(QWidget *parent = nullptr); + ~AtomicConfigDialog() override; + + QStringList checkedFiat(); + QStringList checkedCrypto(); + +private slots: + void selectAll(); + void deselectAll(); + +private: + void setCheckState(QListWidget *widget, Qt::CheckState checkState); + QStringList getChecked(QListWidget *widget); + void fillListWidgets(); + QListWidget* getVisibleListWidget(); + + QScopedPointer ui; +}; + + +#endif //FEATHER_ATOMICCONFIGDIALOG_H diff --git a/src/plugins/atomic/AtomicConfigDialog.ui b/src/plugins/atomic/AtomicConfigDialog.ui new file mode 100644 index 00000000..aa13d923 --- /dev/null +++ b/src/plugins/atomic/AtomicConfigDialog.ui @@ -0,0 +1,153 @@ + + + AtomicConfigDialog + + + + 0 + 0 + 509 + 574 + + + + Atomic config + + + + + + 0 + + + + Fiat + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + Crypto + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + Qt::Vertical + + + + + + + + + Select all + + + + + + + Deselect all + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + AtomicConfigDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AtomicConfigDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/plugins/atomic/AtomicPlugin.cpp b/src/plugins/atomic/AtomicPlugin.cpp new file mode 100644 index 00000000..cd2bdb44 --- /dev/null +++ b/src/plugins/atomic/AtomicPlugin.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "AtomicPlugin.h" +#include "AtomicConfigDialog.h" + +#include "plugins/PluginRegistry.h" + +AtomicPlugin::AtomicPlugin() +{ +} + +void AtomicPlugin::initialize(Wallet *wallet, QObject *parent) { + this->setParent(parent); + m_tab = new AtomicWidget(nullptr); +} + +QString AtomicPlugin::id() { + return "atomic"; +} + +int AtomicPlugin::idx() const { + return 61; +} + +QString AtomicPlugin::parent() { + return {}; +} + +QString AtomicPlugin::displayName() { + return "Atomic"; +} + +QString AtomicPlugin::description() { + return {}; +} + +QString AtomicPlugin::icon() { + return "gnome-calc.png"; +} + +QStringList AtomicPlugin::socketData() { + return {}; +} + +Plugin::PluginType AtomicPlugin::type() { + return Plugin::PluginType::TAB; +} + +QWidget* AtomicPlugin::tab() { + return m_tab; +} + +bool AtomicPlugin::configurable() { + return true; +} + +QDialog* AtomicPlugin::configDialog(QWidget *parent) { + return new AtomicConfigDialog{parent}; +} + +void AtomicPlugin::skinChanged() { + m_tab->skinChanged(); +} + +const bool AtomicPlugin::registered = [] { + PluginRegistry::registerPlugin(AtomicPlugin::create()); + PluginRegistry::getInstance().registerPluginCreator(&AtomicPlugin::create); + return true; +}(); diff --git a/src/plugins/atomic/AtomicPlugin.h b/src/plugins/atomic/AtomicPlugin.h new file mode 100644 index 00000000..abdcd2bd --- /dev/null +++ b/src/plugins/atomic/AtomicPlugin.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef ATOMICPLUGIN_H +#define ATOMICPLUGIN_H + +#include "plugins/Plugin.h" +#include "AtomicWidget.h" + +class AtomicPlugin : public Plugin { +Q_OBJECT + +public: + explicit AtomicPlugin(); + + QString id() override; + int idx() const override; + QString parent() override; + QString displayName() override; + QString description() override; + QString icon() override; + QStringList socketData() override; + PluginType type() override; + QWidget* tab() override; + bool configurable() override; + QDialog* configDialog(QWidget *parent) override; + + void initialize(Wallet *wallet, QObject *parent) override; + + static AtomicPlugin* create() { return new AtomicPlugin(); } + +public slots: + void skinChanged() override; + +private: + AtomicWidget* m_tab = nullptr; + static const bool registered; +}; + + +#endif //ATOMICPLUGIN_H diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp new file mode 100644 index 00000000..e4c5323f --- /dev/null +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "AtomicWidget.h" +#include "ui_AtomicWidget.h" + +#include + +#include "AtomicConfigDialog.h" +#include "utils/AppData.h" +#include "utils/ColorScheme.h" +#include "utils/config.h" +#include "utils/WebsocketNotifier.h" + +AtomicWidget::AtomicWidget(QWidget *parent) + : QWidget(parent) + , ui(new Ui::AtomicWidget) +{ + ui->setupUi(this); + + ui->imageExchange->setBackgroundRole(QPalette::Base); + ui->imageExchange->setAssets(":/assets/images/exchange.png", ":/assets/images/exchange_white.png"); + ui->imageExchange->setScaledContents(true); + ui->imageExchange->setFixedSize(26, 26); + + // validator/locale for input + QString amount_rx = R"(^\d{0,8}[\.]\d{0,12}$)"; + QRegularExpression rx; + rx.setPattern(amount_rx); + QValidator *validator = new QRegularExpressionValidator(rx, this); + ui->lineFrom->setValidator(validator); + ui->lineTo->setValidator(validator); + + connect(&appData()->prices, &Prices::fiatPricesUpdated, this, &AtomicWidget::onPricesReceived); + connect(&appData()->prices, &Prices::cryptoPricesUpdated, this, &AtomicWidget::onPricesReceived); + + connect(ui->lineFrom, &QLineEdit::textEdited, this, [this]{this->convert(false);}); + connect(ui->lineTo, &QLineEdit::textEdited, this, [this]{this->convert(true);}); + + connect(ui->comboAtomicFrom, QOverload::of(&QComboBox::currentIndexChanged), [this]{this->convert(false);}); + connect(ui->comboAtomicTo, QOverload::of(&QComboBox::currentIndexChanged), [this]{this->convert(false);}); + + connect(ui->btn_configure, &QPushButton::clicked, this, &AtomicWidget::showAtomicConfigureDialog); + + QTimer::singleShot(1, [this]{ + this->skinChanged(); + }); + + m_statusTimer.start(5000); + connect(&m_statusTimer, &QTimer::timeout, this, &AtomicWidget::updateStatus); + QPixmap warningIcon = QPixmap(":/assets/images/warning.png"); + ui->icon_warning->setPixmap(warningIcon.scaledToWidth(32, Qt::SmoothTransformation)); + + this->updateStatus(); +} + +void AtomicWidget::convert(bool reverse) { + if (!m_comboBoxInit) + return; + + auto lineFrom = reverse ? ui->lineTo : ui->lineFrom; + auto lineTo = reverse ? ui->lineFrom : ui->lineTo; + + auto comboFrom = reverse ? ui->comboAtomicTo : ui->comboAtomicFrom; + auto comboTo = reverse ? ui->comboAtomicFrom : ui->comboAtomicTo; + + QString symbolFrom = comboFrom->itemText(comboFrom->currentIndex()); + QString symbolTo = comboTo->itemText(comboTo->currentIndex()); + + if (symbolFrom == symbolTo) { + lineTo->setText(lineFrom->text()); + } + + QString amountStr = lineFrom->text(); + double amount = amountStr.toDouble(); + double result = appData()->prices.convert(symbolFrom, symbolTo, amount); + + int precision = 10; + if (appData()->prices.rates.contains(symbolTo)) + precision = 2; + + lineTo->setText(QString::number(result, 'f', precision)); +} + +void AtomicWidget::onPricesReceived() { + if (m_comboBoxInit) + return; + + QList cryptoKeys = appData()->prices.markets.keys(); + QList fiatKeys = appData()->prices.rates.keys(); + if (cryptoKeys.empty() || fiatKeys.empty()) + return; + + ui->btn_configure->setEnabled(true); + this->initComboBox(); + m_comboBoxInit = true; + this->updateStatus(); +} + +void AtomicWidget::initComboBox() { + QList cryptoKeys = appData()->prices.markets.keys(); + QList fiatKeys = appData()->prices.rates.keys(); + + QStringList enabledCrypto = conf()->get(Config::cryptoSymbols).toStringList(); + QStringList filteredCryptoKeys; + for (const auto& symbol : cryptoKeys) { + if (enabledCrypto.contains(symbol)) { + filteredCryptoKeys.append(symbol); + } + } + + QStringList enabledFiat = conf()->get(Config::fiatSymbols).toStringList(); + auto preferredFiat = conf()->get(Config::preferredFiatCurrency).toString(); + if (!enabledFiat.contains(preferredFiat) && fiatKeys.contains(preferredFiat)) { + enabledFiat.append(preferredFiat); + conf()->set(Config::fiatSymbols, enabledFiat); + } + QStringList filteredFiatKeys; + for (const auto &symbol : fiatKeys) { + if (enabledFiat.contains(symbol)) { + filteredFiatKeys.append(symbol); + } + } + + this->setupComboBox(ui->comboAtomicFrom, filteredCryptoKeys, filteredFiatKeys); + this->setupComboBox(ui->comboAtomicTo, filteredCryptoKeys, filteredFiatKeys); + + ui->comboAtomicFrom->setCurrentIndex(ui->comboAtomicFrom->findText("XMR")); + + if (!preferredFiat.isEmpty()) { + ui->comboAtomicTo->setCurrentIndex(ui->comboAtomicTo->findText(preferredFiat)); + } else { + ui->comboAtomicTo->setCurrentIndex(ui->comboAtomicTo->findText("USD")); + } +} + +void AtomicWidget::skinChanged() { + ui->imageExchange->setMode(ColorScheme::hasDarkBackground(this)); +} + +void AtomicWidget::showAtomicConfigureDialog() { + AtomicConfigDialog dialog{this}; + + if (dialog.exec() == QDialog::Accepted) { + this->initComboBox(); + } +} + +void AtomicWidget::setupComboBox(QComboBox *comboBox, const QStringList &crypto, const QStringList &fiat) { + comboBox->clear(); + comboBox->addItems(crypto); + comboBox->insertSeparator(comboBox->count()); + comboBox->addItems(fiat); +} + +void AtomicWidget::updateStatus() { + if (!m_comboBoxInit) { + ui->label_warning->setText("Waiting on exchange data."); + ui->frame_warning->show(); + } + else if (websocketNotifier()->stale(10)) { + ui->label_warning->setText("No new exchange rates received for over 10 minutes."); + ui->frame_warning->show(); + } + else { + ui->frame_warning->hide(); + } +} + +AtomicWidget::~AtomicWidget() = default; \ No newline at end of file diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h new file mode 100644 index 00000000..da65c29f --- /dev/null +++ b/src/plugins/atomic/AtomicWidget.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_ATOMICWIDGET_H +#define FEATHER_ATOMICWIDGET_H + +#include +#include +#include + +namespace Ui { + class AtomicWidget; +} + +class AtomicWidget : public QWidget +{ +Q_OBJECT + +public: + explicit AtomicWidget(QWidget *parent = nullptr); + ~AtomicWidget() override; + +public slots: + void skinChanged(); + +private slots: + void initComboBox(); + void showAtomicConfigureDialog(); + void onPricesReceived(); + +private: + void convert(bool reverse); + void setupComboBox(QComboBox *comboBox, const QStringList &crypto, const QStringList &fiat); + void updateStatus(); + + QScopedPointer ui; + bool m_comboBoxInit = false; + QTimer m_statusTimer; +}; + +#endif // FEATHER_ATOMICWIDGET_H diff --git a/src/plugins/atomic/AtomicWidget.ui b/src/plugins/atomic/AtomicWidget.ui new file mode 100644 index 00000000..2f3236f3 --- /dev/null +++ b/src/plugins/atomic/AtomicWidget.ui @@ -0,0 +1,187 @@ + + + AtomicWidget + + + + 0 + 0 + 800 + 366 + + + + MainWindow + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + icon + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 10 + 20 + + + + + + + + Warning text + + + + + + + + + + 18 + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + true + + + From... + + + + + + + + + + + + exchange image + + + + + + + 0 + + + + + + 0 + 0 + + + + false + + + To... + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Configure + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + DoublePixmapLabel + QLabel +
components.h
+
+
+ + + + fromChanged(QString) + toChanged(QString) + toComboChanged(QString) + +
diff --git a/src/plugins/atomic/AtomicWindow.cpp b/src/plugins/atomic/AtomicWindow.cpp new file mode 100644 index 00000000..8ee497a4 --- /dev/null +++ b/src/plugins/atomic/AtomicWindow.cpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "AtomicWindow.h" +#include "ui_AtomicWindow.h" + +#include "utils/AppData.h" +#include "utils/Icons.h" + +AtomicWindow::AtomicWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::AtomicWindow) +{ + Qt::WindowFlags flags = this->windowFlags(); + this->setWindowFlags(flags|Qt::WindowStaysOnTopHint); // on top + + ui->setupUi(this); + this->setWindowIcon(icons()->icon("gnome-Atomic.png")); +} + +AtomicWindow::~AtomicWindow() = default; \ No newline at end of file diff --git a/src/plugins/atomic/AtomicWindow.h b/src/plugins/atomic/AtomicWindow.h new file mode 100644 index 00000000..acde94c2 --- /dev/null +++ b/src/plugins/atomic/AtomicWindow.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_ATOMICWINDOW_H +#define FEATHER_ATOMICWINDOW_H + +#include + +namespace Ui { + class AtomicWindow; +} + +class AtomicWindow : public QMainWindow +{ +Q_OBJECT + +public: + explicit AtomicWindow(QWidget *parent = nullptr); + ~AtomicWindow() override; + +private: + QScopedPointer ui; +}; + +#endif // FEATHER_ATOMICWINDOW_H + diff --git a/src/plugins/atomic/AtomicWindow.ui b/src/plugins/atomic/AtomicWindow.ui new file mode 100644 index 00000000..74762e95 --- /dev/null +++ b/src/plugins/atomic/AtomicWindow.ui @@ -0,0 +1,49 @@ + + + AtomicWindow + + + + 0 + 0 + 520 + 108 + + + + Atomic Swap + + + + + 9 + + + 9 + + + 9 + + + 9 + + + + + + + + + + AtomicWidget + QWidget +
plugins/atomic/AtomicWidget.h
+ 1 +
+
+ + + + stayOnTop(int) + +
diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 722eb559..0544a39e 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -43,7 +43,7 @@ static const QHash configStrings = { {Config::useOnionNodes,{QS("useOnionNodes"), false}}, // Tabs - {Config::enabledTabs, {QS("enabledTabs"), QStringList{"Home", "History", "Send", "Receive", "Calc"}}}, + {Config::enabledTabs, {QS("enabledTabs"), QStringList{"Home", "History", "Send", "Receive", "Calc", "Atomic"}}}, {Config::showSearchbar,{QS("showSearchbar"), true}}, // History @@ -129,7 +129,7 @@ static const QHash configStrings = { {Config::useLocalTor, {QS("useLocalTor"), false}}, {Config::initSyncThreshold, {QS("initSyncThreshold"), 360}}, - {Config::enabledPlugins, {QS("enabledPlugins"), QStringList{"tickers", "crowdfunding", "bounties", "revuo", "calc", "xmrig"}}}, + {Config::enabledPlugins, {QS("enabledPlugins"), QStringList{"tickers", "crowdfunding", "bounties", "revuo", "calc", "xmrig", "atomic"}}}, {Config::restartRequired, {QS("restartRequired"), false}}, {Config::tickers, {QS("tickers"), QStringList{"XMR", "BTC", "XMR/BTC"}}}, diff --git a/src/wizard/WalletWizard.cpp b/src/wizard/WalletWizard.cpp index e60bfa81..a7b9b021 100644 --- a/src/wizard/WalletWizard.cpp +++ b/src/wizard/WalletWizard.cpp @@ -40,6 +40,8 @@ WalletWizard::WalletWizard(QWidget *parent) auto networkWebsocketPage = new PageNetworkWebsocket(this); auto menuPage = new PageMenu(&m_wizardFields, m_walletKeysFilesModel, this); auto openWalletPage = new PageOpenWallet(m_walletKeysFilesModel, this); + + auto createWallet = new PageWalletFile(&m_wizardFields , this); auto createWalletSeed = new PageWalletSeed(&m_wizardFields, this); auto walletSetPasswordPage = new PageSetPassword(&m_wizardFields, this); diff --git a/src/wizard/WalletWizard.h b/src/wizard/WalletWizard.h index 92cd77d3..eef1a0a9 100644 --- a/src/wizard/WalletWizard.h +++ b/src/wizard/WalletWizard.h @@ -84,7 +84,8 @@ class WalletWizard : public QWizard Page_HardwareDevice, Page_NetworkProxy, Page_NetworkWebsocket, - Page_Plugins + Page_Plugins, + Page_Atomic }; explicit WalletWizard(QWidget *parent = nullptr); From 777ae42ba9747ab6d142386eb91719587c86a935 Mon Sep 17 00:00:00 2001 From: twiddle Date: Sat, 18 May 2024 22:12:23 -0400 Subject: [PATCH 02/26] Plugin Config outline work --- external/feather-docs | 2 +- src/assets.qrc | 1 + src/assets/images/atomic-icon.png | Bin 0 -> 107452 bytes src/plugins/atomic/AtomicConfigDialog.cpp | 40 ++-- src/plugins/atomic/AtomicConfigDialog.h | 9 +- src/plugins/atomic/AtomicConfigDialog.ui | 247 +++++++++------------- src/plugins/atomic/AtomicPlugin.cpp | 2 +- src/plugins/atomic/README.md | 9 + src/utils/config.cpp | 8 + src/utils/config.h | 4 + 10 files changed, 136 insertions(+), 186 deletions(-) create mode 100644 src/assets/images/atomic-icon.png create mode 100644 src/plugins/atomic/README.md diff --git a/external/feather-docs b/external/feather-docs index 9abafde3..6a9c8250 160000 --- a/external/feather-docs +++ b/external/feather-docs @@ -1 +1 @@ -Subproject commit 9abafde37423c204401a2322d152e8b3cc5d267a +Subproject commit 6a9c825056cbec61f91295812556bb0d3c926b9b diff --git a/src/assets.qrc b/src/assets.qrc index 9f915240..b0ab1879 100644 --- a/src/assets.qrc +++ b/src/assets.qrc @@ -40,6 +40,7 @@ assets/images/file.png assets/images/file_manager_32px.png assets/images/gnome-calc.png + assets/images/atomic-icon.png assets/images/hd_32px.png assets/images/history.png assets/images/i2p.png diff --git a/src/assets/images/atomic-icon.png b/src/assets/images/atomic-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9b6b49d4921b63ff0a67d1d7eb4a86d0d2c2c1cc GIT binary patch literal 107452 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 I)3$&3e_=-K<^TWy literal 0 HcmV?d00001 diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp index 1cd7d53c..0b9e2ed3 100644 --- a/src/plugins/atomic/AtomicConfigDialog.cpp +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -6,6 +6,7 @@ #include "utils/AppData.h" #include "utils/config.h" +#include "utils/Networking.h" AtomicConfigDialog::AtomicConfigDialog(QWidget *parent) : WindowModalDialog(parent) @@ -15,33 +16,19 @@ AtomicConfigDialog::AtomicConfigDialog(QWidget *parent) this->fillListWidgets(); - connect(ui->btn_selectAll, &QPushButton::clicked, this, &AtomicConfigDialog::selectAll); - connect(ui->btn_deselectAll, &QPushButton::clicked, this, &AtomicConfigDialog::deselectAll); - + connect(ui->btn_autoInstall,&QPushButton::clicked, this, &AtomicConfigDialog::downloadBinary); + connect(ui->btn_selectFile,&QPushButton::clicked, this, &AtomicConfigDialog::selectBinary); connect(ui->buttonBox, &QDialogButtonBox::accepted, [this]{ - conf()->set(Config::fiatSymbols, this->checkedFiat()); - conf()->set(Config::cryptoSymbols, this->checkedCrypto()); this->accept(); }); this->adjustSize(); } -QStringList AtomicConfigDialog::checkedFiat() { - return this->getChecked(ui->list_fiat); -} -QStringList AtomicConfigDialog::checkedCrypto() { - return this->getChecked(ui->list_crypto); -} -void AtomicConfigDialog::selectAll() { - this->setCheckState(this->getVisibleListWidget(), Qt::Checked); -} -void AtomicConfigDialog::deselectAll() { - this->setCheckState(this->getVisibleListWidget(), Qt::Unchecked); -} + void AtomicConfigDialog::setCheckState(QListWidget *widget, Qt::CheckState checkState) { QListWidgetItem *item; @@ -63,13 +50,7 @@ QStringList AtomicConfigDialog::getChecked(QListWidget *widget) { return checked; } -QListWidget* AtomicConfigDialog::getVisibleListWidget() { - if (ui->tabWidget->currentIndex() == 0) { - return ui->list_fiat; - } else { - return ui->list_crypto; - } -} + void AtomicConfigDialog::fillListWidgets() { QStringList cryptoCurrencies = appData()->prices.markets.keys(); @@ -78,8 +59,6 @@ void AtomicConfigDialog::fillListWidgets() { QStringList checkedCryptoCurrencies = conf()->get(Config::cryptoSymbols).toStringList(); QStringList checkedFiatCurrencies = conf()->get(Config::fiatSymbols).toStringList(); - ui->list_crypto->addItems(cryptoCurrencies); - ui->list_fiat->addItems(fiatCurrencies); auto setChecked = [](QListWidget *widget, const QStringList &checked){ QListWidgetItem *item; @@ -94,8 +73,13 @@ void AtomicConfigDialog::fillListWidgets() { } }; - setChecked(ui->list_crypto, checkedCryptoCurrencies); - setChecked(ui->list_fiat, checkedFiatCurrencies); } +void AtomicConfigDialog::downloadBinary() { + +}; + +void AtomicConfigDialog::selectBinary() { + +}; AtomicConfigDialog::~AtomicConfigDialog() = default; \ No newline at end of file diff --git a/src/plugins/atomic/AtomicConfigDialog.h b/src/plugins/atomic/AtomicConfigDialog.h index a51d28d4..53701567 100644 --- a/src/plugins/atomic/AtomicConfigDialog.h +++ b/src/plugins/atomic/AtomicConfigDialog.h @@ -21,18 +21,13 @@ Q_OBJECT explicit AtomicConfigDialog(QWidget *parent = nullptr); ~AtomicConfigDialog() override; - QStringList checkedFiat(); - QStringList checkedCrypto(); - -private slots: - void selectAll(); - void deselectAll(); private: void setCheckState(QListWidget *widget, Qt::CheckState checkState); QStringList getChecked(QListWidget *widget); void fillListWidgets(); - QListWidget* getVisibleListWidget(); + void downloadBinary(); + void selectBinary(); QScopedPointer ui; }; diff --git a/src/plugins/atomic/AtomicConfigDialog.ui b/src/plugins/atomic/AtomicConfigDialog.ui index aa13d923..dfe1990b 100644 --- a/src/plugins/atomic/AtomicConfigDialog.ui +++ b/src/plugins/atomic/AtomicConfigDialog.ui @@ -1,153 +1,102 @@ - AtomicConfigDialog - - - - 0 - 0 - 509 - 574 - - - - Atomic config - - - - - - 0 - - - - Fiat - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - Crypto - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - Qt::Vertical - - - - - - - - - Select all - - - - - - - Deselect all - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - + AtomicConfigDialog + + + + 0 + 0 + 509 + 574 + + + + Atomic config + + + + + + Qt::Vertical + - - - - buttonBox - accepted() - AtomicConfigDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - AtomicConfigDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - + + + + + + + Auto Install Swap Tool + + + + + + + Select Path to Binary + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + AtomicConfigDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AtomicConfigDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + diff --git a/src/plugins/atomic/AtomicPlugin.cpp b/src/plugins/atomic/AtomicPlugin.cpp index cd2bdb44..ddaf2400 100644 --- a/src/plugins/atomic/AtomicPlugin.cpp +++ b/src/plugins/atomic/AtomicPlugin.cpp @@ -36,7 +36,7 @@ QString AtomicPlugin::description() { } QString AtomicPlugin::icon() { - return "gnome-calc.png"; + return "atomic-icon.png"; } QStringList AtomicPlugin::socketData() { diff --git a/src/plugins/atomic/README.md b/src/plugins/atomic/README.md new file mode 100644 index 00000000..0876758b --- /dev/null +++ b/src/plugins/atomic/README.md @@ -0,0 +1,9 @@ +### Atomic Plugin +Built in xmr-btc atomic swap + +## Installation + +## Usage +Navigate to the Atomic tab after opening your wallet. Click the configure button. + +// Config Settings diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 0544a39e..d78ccb94 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -134,6 +134,14 @@ static const QHash configStrings = { {Config::tickers, {QS("tickers"), QStringList{"XMR", "BTC", "XMR/BTC"}}}, {Config::tickersShowFiatBalance, {QS("tickersShowFiatBalance"), true}}, + + // Atomic + {Config::rendezVous, {QS("rendezVous"), QStringList{"/dns4/xmr-btc-asb.coblox.tech/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi", + "/dnsaddr/atomic.money/p2p/12D3KooWNfiwKyDpAbW7XSgw5xmoMJzMXsJxR91FbUJiuWU1vBGb", + "/dnsaddr/xmr.darkness.su/p2p/12D3KooWLWpQtmuPoQvMJxs6KRGMrc39ohkNbRNaXWJhW3DfQD6e", + "/dnsaddr/swapanarchy.cfd/p2p/12D3KooWMgGjeW7ErQxCQzaeHiXxJn42wegCPFepixEXfBJT1PNS", + "/onion3/spqfqxirmlrhq7gbiwn4jn35c77gu2kof26i6psoc6bbyduol3zty6qd:9841/p2p/12D3KooWM9ipr33nEtxyCBF7fdbHsMrRzHaSf1bEVYzV8XSBSMet"}}}, + {Config::swapPath, {QS("swapPath"), ""}}, }; diff --git a/src/utils/config.h b/src/utils/config.h index 215ec6bc..87fd3ba4 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -148,6 +148,10 @@ class Config : public QObject // Tickers tickers, tickersShowFiatBalance, + + // Atomic Settings + rendezVous, + swapPath, }; enum PrivacyLevel { From da70287dee38d2e9f2b786bedcadc031a23c5561 Mon Sep 17 00:00:00 2001 From: twiddle Date: Sat, 18 May 2024 22:15:06 -0400 Subject: [PATCH 03/26] Plugin Config outline work --- external/feather-docs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/feather-docs b/external/feather-docs index 6a9c8250..1c8e9318 160000 --- a/external/feather-docs +++ b/external/feather-docs @@ -1 +1 @@ -Subproject commit 6a9c825056cbec61f91295812556bb0d3c926b9b +Subproject commit 1c8e9318b29b18b08e0302c1e34e4f3581e7895c From 435a75d453bbbcb524de7c8aa2df67d3a819ce57 Mon Sep 17 00:00:00 2001 From: twiddle Date: Tue, 21 May 2024 23:34:03 -0400 Subject: [PATCH 04/26] Swap tool auto downloader working. // TODO Later Add in Browse button Add ui reflection of download progess/occuring --- CMakeLists.txt | 3 +- src/CMakeLists.txt | 8 ++ src/plugins/atomic/AtomicConfigDialog.cpp | 128 +++++++++++++++++++++- src/plugins/atomic/AtomicConfigDialog.h | 12 ++ src/plugins/atomic/README.md | 3 + src/utils/config.cpp | 9 +- src/utils/config.h | 2 + 7 files changed, 162 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fa962f6f..8becdef9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ set(COPYRIGHT_HOLDERS "The Monero Project") option(STATIC "Link libraries statically, requires static Qt" OFF) option(SELF_CONTAINED "Disable when building Feather for packages" OFF) option(TOR_DIR "Directory containing Tor binaries to embed inside Feather" OFF) -option(CHECK_UPDATES "Enable checking for application updates" OFF) +option(CHECK_UPDATES "Enable checking for application updates" ON) option(PLATFORM_INSTALLER "Built-in updater fetches installer (windows-only)" OFF) option(USE_DEVICE_TREZOR "Trezor support compilation" ON) option(DONATE_BEG "Prompt donation window every once in a while" OFF) @@ -119,6 +119,7 @@ endif() # libzip if(CHECK_UPDATES) set(ZLIB_USE_STATIC_LIBS "ON") + message("libzip inclueded") find_package(ZLIB REQUIRED) find_path(LIBZIP_INCLUDE_DIRS zip.h) find_library(LIBZIP_LIBRARIES zip) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 49384469..416e56e6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,8 @@ find_package(Qt6 REQUIRED COMPONENTS ${QT_COMPONENTS}) set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication") add_subdirectory(third-party/singleapplication) + + if (CHECK_UPDATES) add_subdirectory(openpgp) endif() @@ -299,6 +301,12 @@ if (WITH_SCANNER) ) endif() + + FIND_PACKAGE(LibArchive REQUIRED) + INCLUDE_DIRECTORIES(${LibArchive_INCLUDE_DIR}) + target_link_libraries(feather PRIVATE ${LibArchive_LIBRARIES}) + + if(STATIC AND APPLE) target_link_libraries(feather PRIVATE Qt6::QDarwinCameraPermissionPlugin) endif() diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp index 0b9e2ed3..6e13216c 100644 --- a/src/plugins/atomic/AtomicConfigDialog.cpp +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -4,6 +4,14 @@ #include "AtomicConfigDialog.h" #include "ui_AtomicConfigDialog.h" +#include +#include +#include +#include +#include +#include +#include + #include "utils/AppData.h" #include "utils/config.h" #include "utils/Networking.h" @@ -76,9 +84,127 @@ void AtomicConfigDialog::fillListWidgets() { } void AtomicConfigDialog::downloadBinary() { - + auto* network = new Networking(this); + download = new QTemporaryFile(this); + download->open(); + tempFile = download->fileName(); + QString url; + auto operatingSystem = Config::instance()->get(Config::operatingSystem).toString().toStdString(); + if(strcmp("WIN",operatingSystem.c_str()) == 0) { + // HARD CODED DOWNload URL CHANGE IF PROBLEMS + url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.12.3/swap_0.12.3_Windows_x86_64.zip"); + } else if (strcmp("LINUX",operatingSystem.c_str())==0){ + url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.12.3/swap_0.12.3_Linux_x86_64.tar"); + } else { + url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.12.3/swap_0.12.3_Darwin_x86_64.tar"); + } + archive = network->get(this, url); + QStringList answer; + connect(archive,&QNetworkReply::readyRead, this, [&]{ + QByteArray data= archive->readAll(); + qDebug() << "received data of size: " << data.size(); + download->write(data.constData(), data.size()); + }); + connect(archive, &QNetworkReply::finished, + this,&AtomicConfigDialog::extract); }; +void AtomicConfigDialog::extract() { + + + qDebug() << "extracting"; + download->close(); + archive->deleteLater(); + + auto swapPath = Config::defaultConfigDir().absolutePath(); + swapPath.append("/swapTool"); + QFile binaryFile(swapPath); + binaryFile.open(QIODevice::WriteOnly); + auto operatingSystem = Config::instance()->get(Config::operatingSystem).toString().toStdString(); + if(strcmp("WIN",operatingSystem.c_str()) == 0) { + // UNZIP + zip *z = zip_open(tempFile.toStdString().c_str(), 0, 0); + + //Search for the file of given name + const char *name = "swap.exe"; + struct zip_stat st; + zip_stat_init(&st); + zip_stat(z, name, 0, &st); + int total = st.size; + int wrote = 0; + //Alloc memory for its uncompressed contents + char *contents = new char[BUFSIZ]; + zip_file *f = zip_fopen(z, name, 0); + while (wrote < total) { + //Read the compressed file + zip_fread(f, contents, BUFSIZ); + binaryFile.write(contents); + wrote += BUFSIZ; + } + zip_fclose(f); + //And close the archive + zip_close(z); + Config::instance()->set(Config::swapPath,swapPath); + } else { + + struct archive *a; + struct archive *ext; + struct archive_entry *entry; + int r; + std::string savePath; + a = archive_read_new(); + ext = archive_write_disk_new(); + archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_PERM); + + archive_read_support_format_tar(a); + r = archive_read_open_filename(a, tempFile.toStdString().c_str(), 10240); + for (;;) { + r = archive_read_next_header(a, &entry); + if (r == ARCHIVE_EOF) + break; + savePath = Config::defaultConfigDir().absolutePath().toStdString().append("/").append(archive_entry_pathname(entry)); + qDebug() << savePath; + archive_entry_set_pathname(entry, savePath.c_str()); + r = archive_write_header(ext, entry); + copy_data(a, ext); + r = archive_write_finish_entry(ext); + } + + archive_read_close(a); + archive_read_free(a); + + archive_write_close(ext); + archive_write_free(ext); + Config::instance()->set(Config::swapPath, QString(savePath.c_str())); + } + qDebug() << "Finished"; + binaryFile.close(); + +}; +int +AtomicConfigDialog::copy_data(struct archive *ar, struct archive *aw) +{ + int r; + const void *buff; + size_t size; +#if ARCHIVE_VERSION_NUMBER >= 3000000 + int64_t offset; +#else + off_t offset; +#endif + + for (;;) { + r = archive_read_data_block(ar, &buff, &size, &offset); + if (r == ARCHIVE_EOF) + return (ARCHIVE_OK); + if (r != ARCHIVE_OK) + return (r); + r = archive_write_data_block(aw, buff, size, offset); + if (r != ARCHIVE_OK) { + return (r); + } + } +} void AtomicConfigDialog::selectBinary() { }; diff --git a/src/plugins/atomic/AtomicConfigDialog.h b/src/plugins/atomic/AtomicConfigDialog.h index 53701567..0d22aae3 100644 --- a/src/plugins/atomic/AtomicConfigDialog.h +++ b/src/plugins/atomic/AtomicConfigDialog.h @@ -6,7 +6,10 @@ #include #include +#include +#include +#include #include "components.h" namespace Ui { @@ -21,6 +24,8 @@ Q_OBJECT explicit AtomicConfigDialog(QWidget *parent = nullptr); ~AtomicConfigDialog() override; +public slots: + void extract(); private: void setCheckState(QListWidget *widget, Qt::CheckState checkState); @@ -28,8 +33,15 @@ Q_OBJECT void fillListWidgets(); void downloadBinary(); void selectBinary(); + int copy_data(struct archive *ar, struct archive *aw); QScopedPointer ui; + + QNetworkReply* archive; + QString tempFile; + QTemporaryFile* download; + + }; diff --git a/src/plugins/atomic/README.md b/src/plugins/atomic/README.md index 0876758b..cb189832 100644 --- a/src/plugins/atomic/README.md +++ b/src/plugins/atomic/README.md @@ -3,6 +3,9 @@ Built in xmr-btc atomic swap ## Installation +## Hacking +sudo apt install +Install KArchive ## Usage Navigate to the Atomic tab after opening your wallet. Click the configure button. diff --git a/src/utils/config.cpp b/src/utils/config.cpp index d78ccb94..3ad5eb31 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -8,7 +8,13 @@ #include "utils/os/tails.h" #define QS QStringLiteral - +#if defined(Q_OS_WIN64) +#define OS "WINDOWS" +#elif defined(Q_OS_DARWIN) +#define OS "MAC" +#else +#define OS "LINUX" +#endif struct ConfigDirective { QString name; @@ -142,6 +148,7 @@ static const QHash configStrings = { "/dnsaddr/swapanarchy.cfd/p2p/12D3KooWMgGjeW7ErQxCQzaeHiXxJn42wegCPFepixEXfBJT1PNS", "/onion3/spqfqxirmlrhq7gbiwn4jn35c77gu2kof26i6psoc6bbyduol3zty6qd:9841/p2p/12D3KooWM9ipr33nEtxyCBF7fdbHsMrRzHaSf1bEVYzV8XSBSMet"}}}, {Config::swapPath, {QS("swapPath"), ""}}, + {Config::operatingSystem, {QS("operatingSystem"), OS}}, }; diff --git a/src/utils/config.h b/src/utils/config.h index 87fd3ba4..dcf6f30f 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -11,6 +11,7 @@ #include #include + class Config : public QObject { Q_OBJECT @@ -152,6 +153,7 @@ class Config : public QObject // Atomic Settings rendezVous, swapPath, + operatingSystem, }; enum PrivacyLevel { From e6aac4eb25f79f47f45c15d246d1f88b64cb6756 Mon Sep 17 00:00:00 2001 From: twiddle Date: Wed, 22 May 2024 13:55:51 -0400 Subject: [PATCH 05/26] Swap tool auto downloader finished, browse finished, and ui updates --- src/plugins/atomic/AtomicConfigDialog.cpp | 90 ++++++++--------------- src/plugins/atomic/AtomicConfigDialog.h | 4 - src/plugins/atomic/AtomicConfigDialog.ui | 77 ++++++++++--------- 3 files changed, 74 insertions(+), 97 deletions(-) diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp index 6e13216c..6b36de53 100644 --- a/src/plugins/atomic/AtomicConfigDialog.cpp +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -9,10 +9,10 @@ #include #include #include -#include + #include +#include -#include "utils/AppData.h" #include "utils/config.h" #include "utils/Networking.h" @@ -22,10 +22,28 @@ AtomicConfigDialog::AtomicConfigDialog(QWidget *parent) { ui->setupUi(this); - this->fillListWidgets(); - - connect(ui->btn_autoInstall,&QPushButton::clicked, this, &AtomicConfigDialog::downloadBinary); - connect(ui->btn_selectFile,&QPushButton::clicked, this, &AtomicConfigDialog::selectBinary); + ui->downloadLabel->setVisible(false); + connect(ui->btn_autoInstall,&QPushButton::clicked, this, [this] { + /* + QMessageBox downloadPopup = QMessageBox(this); + qDebug() << "opening popup"; + downloadPopup.setText("Downloading swap tool, this usually takes about a minute or two, please wait"); + downloadPopup.setWindowTitle("Swap tool download"); + downloadPopup.show(); + qApp->processEvents(); + */ + AtomicConfigDialog::downloadBinary(); + }); + connect(ui->btn_selectFile,&QPushButton::clicked, this, [this] { + QString path = QFileDialog::getOpenFileName(this, "Select swap binary file", + Config::defaultConfigDir().absolutePath(), + "Binary Executable (*)"); + Config::instance()->set(Config::swapPath, path); + if(path.isEmpty()){ + return; + } + close(); + }); connect(ui->buttonBox, &QDialogButtonBox::accepted, [this]{ this->accept(); }); @@ -33,56 +51,6 @@ AtomicConfigDialog::AtomicConfigDialog(QWidget *parent) this->adjustSize(); } - - - - - -void AtomicConfigDialog::setCheckState(QListWidget *widget, Qt::CheckState checkState) { - QListWidgetItem *item; - for (int i=0; i < widget->count(); i++) { - item = widget->item(i); - item->setCheckState(checkState); - } -} - -QStringList AtomicConfigDialog::getChecked(QListWidget *widget) { - QStringList checked; - QListWidgetItem *item; - for (int i=0; i < widget->count(); i++) { - item = widget->item(i); - if (item->checkState() == Qt::Checked) { - checked.append(item->text()); - } - } - return checked; -} - - - -void AtomicConfigDialog::fillListWidgets() { - QStringList cryptoCurrencies = appData()->prices.markets.keys(); - QStringList fiatCurrencies = appData()->prices.rates.keys(); - - QStringList checkedCryptoCurrencies = conf()->get(Config::cryptoSymbols).toStringList(); - QStringList checkedFiatCurrencies = conf()->get(Config::fiatSymbols).toStringList(); - - - auto setChecked = [](QListWidget *widget, const QStringList &checked){ - QListWidgetItem *item; - for (int i=0; i < widget->count(); i++) { - item = widget->item(i); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - if (checked.contains(item->text())) { - item->setCheckState(Qt::Checked); - } else { - item->setCheckState(Qt::Unchecked); - } - } - }; - -} - void AtomicConfigDialog::downloadBinary() { auto* network = new Networking(this); download = new QTemporaryFile(this); @@ -98,9 +66,11 @@ void AtomicConfigDialog::downloadBinary() { } else { url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.12.3/swap_0.12.3_Darwin_x86_64.tar"); } + archive = network->get(this, url); + ui->downloadLabel->setVisible(true); QStringList answer; - connect(archive,&QNetworkReply::readyRead, this, [&]{ + connect(archive,&QNetworkReply::readyRead, this, [this]{ QByteArray data= archive->readAll(); qDebug() << "received data of size: " << data.size(); download->write(data.constData(), data.size()); @@ -111,7 +81,7 @@ void AtomicConfigDialog::downloadBinary() { void AtomicConfigDialog::extract() { - + ui->downloadLabel->setText("Download Successful, extracting binary to final destination"); qDebug() << "extracting"; download->close(); archive->deleteLater(); @@ -179,6 +149,8 @@ void AtomicConfigDialog::extract() { } qDebug() << "Finished"; binaryFile.close(); + ui->downloadLabel->setText("Swap tool installation complete, Atomic swaps are ready !"); + }; int @@ -205,7 +177,5 @@ AtomicConfigDialog::copy_data(struct archive *ar, struct archive *aw) } } } -void AtomicConfigDialog::selectBinary() { -}; AtomicConfigDialog::~AtomicConfigDialog() = default; \ No newline at end of file diff --git a/src/plugins/atomic/AtomicConfigDialog.h b/src/plugins/atomic/AtomicConfigDialog.h index 0d22aae3..69023677 100644 --- a/src/plugins/atomic/AtomicConfigDialog.h +++ b/src/plugins/atomic/AtomicConfigDialog.h @@ -28,11 +28,7 @@ public slots: void extract(); private: - void setCheckState(QListWidget *widget, Qt::CheckState checkState); - QStringList getChecked(QListWidget *widget); - void fillListWidgets(); void downloadBinary(); - void selectBinary(); int copy_data(struct archive *ar, struct archive *aw); QScopedPointer ui; diff --git a/src/plugins/atomic/AtomicConfigDialog.ui b/src/plugins/atomic/AtomicConfigDialog.ui index dfe1990b..e4b0ec14 100644 --- a/src/plugins/atomic/AtomicConfigDialog.ui +++ b/src/plugins/atomic/AtomicConfigDialog.ui @@ -6,7 +6,7 @@ 0 0 - 509 + 514 574 @@ -17,46 +17,57 @@ - Qt::Vertical + Qt::Orientation::Vertical - + - - - Auto Install Swap Tool - - + + + + + Auto Install Swap Tool + + + + + + + Select Path to Binary + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Close + + + + - + - Select Path to Binary - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + Downloading swap tool ... this usually takes await cause of tor routing From 0018b5a8692c8ddc4586d2710367ce720bfb14b2 Mon Sep 17 00:00:00 2001 From: twiddle Date: Sun, 26 May 2024 20:19:29 -0400 Subject: [PATCH 06/26] Offer Book updating half done. Need to fix refreshing gui to reflect new order. --- CMakeLists.txt | 4 +- src/CMakeLists.txt | 4 +- src/plugins/atomic/AtomicPlugin.h | 2 +- src/plugins/atomic/AtomicWidget.cpp | 101 +++++++- src/plugins/atomic/AtomicWidget.h | 7 + src/plugins/atomic/AtomicWidget.ui | 366 ++++++++++++++-------------- src/plugins/atomic/Offer.h | 20 ++ src/plugins/atomic/OfferModel.cpp | 124 ++++++++++ src/plugins/atomic/OfferModel.h | 42 ++++ src/plugins/atomic/README.md | 9 +- src/utils/config.cpp | 9 +- 11 files changed, 494 insertions(+), 194 deletions(-) create mode 100644 src/plugins/atomic/Offer.h create mode 100644 src/plugins/atomic/OfferModel.cpp create mode 100644 src/plugins/atomic/OfferModel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8becdef9..22745351 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,7 +26,7 @@ option(DONATE_BEG "Prompt donation window every once in a while" OFF) option(WITH_SCANNER "Enable webcam QR scanner" ON) option(STACK_TRACE "Dump stack trace on crash (Linux only)" OFF) -# Plugins +# option(WITH_PLUGIN_HOME "Include Home tab plugin" ON) option(WITH_PLUGIN_TICKERS "Include Tickers Home plugin" ON) option(WITH_PLUGIN_CROWDFUNDING "Include Crowdfunding Home plugin" ON) @@ -117,7 +117,7 @@ if(WITH_SCANNER) endif() # libzip -if(CHECK_UPDATES) +if(CHECK_UPDATES OR WITH_PLUGIN_ATOMIC) set(ZLIB_USE_STATIC_LIBS "ON") message("libzip inclueded") find_package(ZLIB REQUIRED) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 416e56e6..8a9410ce 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -301,11 +301,11 @@ if (WITH_SCANNER) ) endif() - +if (WITH_PLUGIN_ATOMIC) FIND_PACKAGE(LibArchive REQUIRED) INCLUDE_DIRECTORIES(${LibArchive_INCLUDE_DIR}) target_link_libraries(feather PRIVATE ${LibArchive_LIBRARIES}) - +endif() if(STATIC AND APPLE) target_link_libraries(feather PRIVATE Qt6::QDarwinCameraPermissionPlugin) diff --git a/src/plugins/atomic/AtomicPlugin.h b/src/plugins/atomic/AtomicPlugin.h index abdcd2bd..10cf4a3c 100644 --- a/src/plugins/atomic/AtomicPlugin.h +++ b/src/plugins/atomic/AtomicPlugin.h @@ -6,6 +6,7 @@ #include "plugins/Plugin.h" #include "AtomicWidget.h" +#include "Offer.h" class AtomicPlugin : public Plugin { Q_OBJECT @@ -26,7 +27,6 @@ Q_OBJECT QDialog* configDialog(QWidget *parent) override; void initialize(Wallet *wallet, QObject *parent) override; - static AtomicPlugin* create() { return new AtomicPlugin(); } public slots: diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index e4c5323f..5605d30e 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -5,8 +5,10 @@ #include "ui_AtomicWidget.h" #include +#include #include "AtomicConfigDialog.h" +#include "OfferModel.h" #include "utils/AppData.h" #include "utils/ColorScheme.h" #include "utils/config.h" @@ -15,6 +17,8 @@ AtomicWidget::AtomicWidget(QWidget *parent) : QWidget(parent) , ui(new Ui::AtomicWidget) + , o_model(new OfferModel(this)) + , offerList(new QList>()) { ui->setupUi(this); @@ -30,7 +34,15 @@ AtomicWidget::AtomicWidget(QWidget *parent) QValidator *validator = new QRegularExpressionValidator(rx, this); ui->lineFrom->setValidator(validator); ui->lineTo->setValidator(validator); - + ui->offerBookTable->setModel(o_model); + ui->offerBookTable->setSortingEnabled(true); + ui->offerBookTable->sortByColumn(0, Qt::SortOrder::AscendingOrder); + ui->offerBookTable->verticalHeader()->setVisible(false); + ui->offerBookTable->setSelectionBehavior(QAbstractItemView::SelectRows); + + ui->offerBookTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->offerBookTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); + //ui->offerBookTable-> connect(&appData()->prices, &Prices::fiatPricesUpdated, this, &AtomicWidget::onPricesReceived); connect(&appData()->prices, &Prices::cryptoPricesUpdated, this, &AtomicWidget::onPricesReceived); @@ -42,6 +54,40 @@ AtomicWidget::AtomicWidget(QWidget *parent) connect(ui->btn_configure, &QPushButton::clicked, this, &AtomicWidget::showAtomicConfigureDialog); + connect(ui->btn_refreshOffer, &QPushButton::clicked, this, [this]{ + offerList->clear(); + + auto m_instance = Config::instance(); + QStringList pointList = m_instance->get(Config::rendezVous).toStringList(); + for(QString point :pointList) + AtomicWidget::list(point); + + /* + QList> tempList; + for (int i=0; iappend(offer); + } + } + qDebug() << "done"; + + /*for(auto offer: AtomicWidget::list(Config::instance()->get(Config::rendezVous).toStringList()[0])){ + offerList->append(offer); + } + o_model->updateOffers(*offerList); + + */ + }); + QTimer::singleShot(1, [this]{ this->skinChanged(); }); @@ -167,4 +213,57 @@ void AtomicWidget::updateStatus() { } } +void AtomicWidget::list(QString rendezvous) { + QStringList arguments; + QList> list; + auto m_instance = Config::instance(); + arguments << "--data-base-dir"; + arguments << Config::defaultConfigDir().absolutePath(); + // Remove after testing + //arguments << "--testnet"; + arguments << "-j"; + arguments << "list-sellers"; + arguments << "--tor-socks5-port"; + arguments << m_instance->get(Config::socks5Port).toString(); + arguments << "--rendezvous-point"; + arguments << rendezvous; + auto *swap = new QProcess(); + swap->setReadChannel(QProcess::StandardOutput); + //swap->start(m_instance->get(Config::swapPath).toString(), arguments); + connect(swap, &QProcess::finished, this, [this, swap]{ + QJsonDocument parsedLine; + QJsonParseError parseError; + QList> list; + qDebug() << "Subprocess has finished"; + auto output = QString::fromLocal8Bit(swap->readAllStandardError()); + qDebug() << "Crashes before splitting"; + auto lines = output.split(QRegularExpression("[\r\n]"),Qt::SkipEmptyParts); + qDebug() << lines.size(); + qDebug() << "parsing Output"; + + + for(auto line : lines){ + qDebug() << line; + if(line.contains("status")){ + qDebug() << "status contained"; + parsedLine = QJsonDocument::fromJson(line.toLocal8Bit(), &parseError ); + if (parsedLine["fields"]["status"].toString().contains("Online")){ + OfferEntry entry = {parsedLine["fields"]["price"].toDouble(),parsedLine["fields"]["min_quantity"].toDouble(),parsedLine["fields"]["max_quantity"].toDouble(), parsedLine["fields"]["address"].toString()}; + offerList->append(QSharedPointer(&entry)); + qDebug() << &entry; + } + } + qDebug() << "next line"; + } + qDebug() << "exits fine"; + swap->close(); + return list; + }); + swap->start("/home/dev/.config/feather/swap", arguments); + //swap->waitForFinished(120000); + + + +} + AtomicWidget::~AtomicWidget() = default; \ No newline at end of file diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h index da65c29f..aaf1b9bb 100644 --- a/src/plugins/atomic/AtomicWidget.h +++ b/src/plugins/atomic/AtomicWidget.h @@ -7,6 +7,10 @@ #include #include #include +#include +#include + +#include "OfferModel.h" namespace Ui { class AtomicWidget; @@ -19,6 +23,7 @@ Q_OBJECT public: explicit AtomicWidget(QWidget *parent = nullptr); ~AtomicWidget() override; + void list(QString rendezvous); public slots: void skinChanged(); @@ -36,6 +41,8 @@ private slots: QScopedPointer ui; bool m_comboBoxInit = false; QTimer m_statusTimer; + OfferModel *o_model; + QList> *offerList; }; #endif // FEATHER_ATOMICWIDGET_H diff --git a/src/plugins/atomic/AtomicWidget.ui b/src/plugins/atomic/AtomicWidget.ui index 2f3236f3..aa5a21e6 100644 --- a/src/plugins/atomic/AtomicWidget.ui +++ b/src/plugins/atomic/AtomicWidget.ui @@ -1,187 +1,191 @@ - AtomicWidget - - - - 0 - 0 - 800 - 366 - + AtomicWidget + + + + 0 + 0 + 800 + 366 + + + + MainWindow + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + + 0 + 0 + - - MainWindow + + icon - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - 0 - 0 - - - - icon - - - - - - - Qt::Horizontal - - - QSizePolicy::Maximum - - - - 10 - 20 - - - - - - - - Warning text - - - - - - - - - - 18 - - - - - 0 - - - 0 - - - - - - 0 - 0 - - - - true - - - From... - - - - - - - - - - - - exchange image - - - - - - - 0 - - - - - - 0 - 0 - - - - false - - - To... - - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Configure - - - - - - - - - Qt::Vertical - - - - 20 - 0 - - - - - + + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Maximum + + + + 10 + 20 + + + + + + + + Warning text + + + + - - - DoublePixmapLabel - QLabel -
components.h
-
-
- - - - fromChanged(QString) - toChanged(QString) - toComboChanged(QString) - +
+ + + + 18 + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + true + + + From... + + + + + + + + + + + + exchange image + + + + + + + 0 + + + + + + 0 + 0 + + + + false + + + To... + + + + + + + + + + + + + + + + + + + Refresh Offer Book + + + + + + + Add Rendezvous Point + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Configure + + + + + +
+
+ + + DoublePixmapLabel + QLabel +
components.h
+
+
+ + + + fromChanged(QString) + toChanged(QString) + toComboChanged(QString) +
diff --git a/src/plugins/atomic/Offer.h b/src/plugins/atomic/Offer.h new file mode 100644 index 00000000..09400416 --- /dev/null +++ b/src/plugins/atomic/Offer.h @@ -0,0 +1,20 @@ +// +// Created by dev on 5/23/24. +// + +#ifndef FEATHER_OFFER_H +#define FEATHER_OFFER_H + +#include + +struct OfferEntry { + OfferEntry(double price, double min, double max, QString address ) + : price(price), min(min), max(max), address(std::move(address)) {}; + + double price; + double min; + double max; + QString address; +}; + +#endif //FEATHER_OFFER_H \ No newline at end of file diff --git a/src/plugins/atomic/OfferModel.cpp b/src/plugins/atomic/OfferModel.cpp new file mode 100644 index 00000000..e4092c82 --- /dev/null +++ b/src/plugins/atomic/OfferModel.cpp @@ -0,0 +1,124 @@ +// +// Created by dev on 5/23/24. +// + +#include "OfferModel.h" + + +OfferModel::OfferModel(QObject *parent) + : QAbstractTableModel(parent) +{ + +} + +void OfferModel::clear() { + beginResetModel(); + + m_offers.clear(); + + endResetModel(); +} + +void OfferModel::updateOffers(const QList> &posts) { + beginResetModel(); + qDebug() << "updating Offers"; + m_offers.clear(); + for (const auto& post : posts) { + m_offers.push_back(post); + } + + endResetModel(); +} + +int OfferModel::rowCount(const QModelIndex &parent) const{ + if (parent.isValid()) { + return 0; + } + return m_offers.count(); +} + +int OfferModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return OfferModel::ModelColumn::COUNT; +} + +QVariant OfferModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_offers.count()) + return {}; + + QSharedPointer post = m_offers.at(index.row()); + + if(role == Qt::DisplayRole || role == Qt::UserRole) { + switch(index.column()) { + case Price: { + if (role == Qt::UserRole) { + return post->price; + } + return QString::number(post->price); + } + case Address: + return post->address; + case Min_Amount:{ + if (role == Qt::UserRole) { + return post->min; + } + return QString::number(post->min); + } + case Max_Amount: { + if (role == Qt::UserRole) { + return post->max; + } + return QString::number(post->max); + } + default: + return {}; + } + } + else if (role == Qt::TextAlignmentRole) { + switch(index.column()) { + case Price: + case Min_Amount: + case Max_Amount: + return Qt::AlignRight; + default: + return {}; + } + } + return {}; +} + +QVariant OfferModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) { + return QVariant(); + } + if (orientation == Qt::Horizontal) + { + switch(section) { + case Price: + return QString("Price (BTC) "); + case Min_Amount: + return QString("Min Amount (BTC)"); + case Max_Amount: + return QString("Max Amount (BTC)"); + case Address: + return QString("P2P Vendor Address"); + default: + return QVariant(); + } + } + return QVariant(); +} + +QSharedPointer OfferModel::post(int row) { + if (row < 0 || row >= m_offers.size()) { + qCritical("%s: no reddit post for index %d", __FUNCTION__, row); + return QSharedPointer(); + } + + return m_offers.at(row); +} \ No newline at end of file diff --git a/src/plugins/atomic/OfferModel.h b/src/plugins/atomic/OfferModel.h new file mode 100644 index 00000000..d6aded3f --- /dev/null +++ b/src/plugins/atomic/OfferModel.h @@ -0,0 +1,42 @@ +// +// Created by dev on 5/23/24. +// + +#ifndef FEATHER_OFFERMODEL_H +#define FEATHER_OFFERMODEL_H +#include + +#include "Offer.h" + +class OfferModel : public QAbstractTableModel +{ +Q_OBJECT + +public: + enum ModelColumn + { + Price = 0, + Min_Amount, + Max_Amount, + Address, + COUNT + }; + + explicit OfferModel(QObject *parent); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + void clear(); + void updateOffers(const QList>& posts); + + QSharedPointer post(int row); + +private: + QList> m_offers; +}; + + +#endif //FEATHER_OFFERMODEL_H diff --git a/src/plugins/atomic/README.md b/src/plugins/atomic/README.md index cb189832..00ebe754 100644 --- a/src/plugins/atomic/README.md +++ b/src/plugins/atomic/README.md @@ -4,8 +4,13 @@ Built in xmr-btc atomic swap ## Installation ## Hacking -sudo apt install -Install KArchive +sudo apt install libarchive-dev + +Functions used to control swap (AtomicPlugin.cpp) + +withdraw(btcaddress) - withdraw btc +list(rendezvous point) - list sellers at rendezvous + ## Usage Navigate to the Atomic tab after opening your wallet. Click the configure button. diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 3ad5eb31..e7878dce 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -142,11 +142,10 @@ static const QHash configStrings = { {Config::tickersShowFiatBalance, {QS("tickersShowFiatBalance"), true}}, // Atomic - {Config::rendezVous, {QS("rendezVous"), QStringList{"/dns4/xmr-btc-asb.coblox.tech/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi", - "/dnsaddr/atomic.money/p2p/12D3KooWNfiwKyDpAbW7XSgw5xmoMJzMXsJxR91FbUJiuWU1vBGb", - "/dnsaddr/xmr.darkness.su/p2p/12D3KooWLWpQtmuPoQvMJxs6KRGMrc39ohkNbRNaXWJhW3DfQD6e", - "/dnsaddr/swapanarchy.cfd/p2p/12D3KooWMgGjeW7ErQxCQzaeHiXxJn42wegCPFepixEXfBJT1PNS", - "/onion3/spqfqxirmlrhq7gbiwn4jn35c77gu2kof26i6psoc6bbyduol3zty6qd:9841/p2p/12D3KooWM9ipr33nEtxyCBF7fdbHsMrRzHaSf1bEVYzV8XSBSMet"}}}, + {Config::rendezVous, {QS("rendezVous"), QStringList{"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", + "/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs", + "/dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o", + "/dns4/swap.sethforprivacy.com/tcp/8888/p2p/12D3KooWCULyZKuV9YEkb6BX8FuwajdvktSzmMg4U5ZX2uYZjHeu"}}}, {Config::swapPath, {QS("swapPath"), ""}}, {Config::operatingSystem, {QS("operatingSystem"), OS}}, }; From ce38f3e36e9a2efa0d549a5029332322183b4adf Mon Sep 17 00:00:00 2001 From: twiddle Date: Wed, 29 May 2024 15:36:03 -0400 Subject: [PATCH 07/26] Offer Book updating finished. Minor ui additions. --- src/plugins/atomic/AtomicWidget.cpp | 154 ++++++-------------------- src/plugins/atomic/AtomicWidget.h | 4 - src/plugins/atomic/AtomicWidget.ui | 160 ++++++++++------------------ 3 files changed, 89 insertions(+), 229 deletions(-) diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 5605d30e..908ce95f 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "AtomicConfigDialog.h" #include "OfferModel.h" @@ -22,18 +23,12 @@ AtomicWidget::AtomicWidget(QWidget *parent) { ui->setupUi(this); - ui->imageExchange->setBackgroundRole(QPalette::Base); - ui->imageExchange->setAssets(":/assets/images/exchange.png", ":/assets/images/exchange_white.png"); - ui->imageExchange->setScaledContents(true); - ui->imageExchange->setFixedSize(26, 26); // validator/locale for input QString amount_rx = R"(^\d{0,8}[\.]\d{0,12}$)"; QRegularExpression rx; rx.setPattern(amount_rx); QValidator *validator = new QRegularExpressionValidator(rx, this); - ui->lineFrom->setValidator(validator); - ui->lineTo->setValidator(validator); ui->offerBookTable->setModel(o_model); ui->offerBookTable->setSortingEnabled(true); ui->offerBookTable->sortByColumn(0, Qt::SortOrder::AscendingOrder); @@ -42,21 +37,18 @@ AtomicWidget::AtomicWidget(QWidget *parent) ui->offerBookTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); ui->offerBookTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); - //ui->offerBookTable-> - connect(&appData()->prices, &Prices::fiatPricesUpdated, this, &AtomicWidget::onPricesReceived); - connect(&appData()->prices, &Prices::cryptoPricesUpdated, this, &AtomicWidget::onPricesReceived); - - connect(ui->lineFrom, &QLineEdit::textEdited, this, [this]{this->convert(false);}); - connect(ui->lineTo, &QLineEdit::textEdited, this, [this]{this->convert(true);}); + ui->btn_configure->setEnabled(true); - connect(ui->comboAtomicFrom, QOverload::of(&QComboBox::currentIndexChanged), [this]{this->convert(false);}); - connect(ui->comboAtomicTo, QOverload::of(&QComboBox::currentIndexChanged), [this]{this->convert(false);}); + if (!Config::instance()->get(Config::swapPath).toString().isEmpty()) + ui->meta_label->setText("Refresh offer book before swapping to prevent errors"); connect(ui->btn_configure, &QPushButton::clicked, this, &AtomicWidget::showAtomicConfigureDialog); connect(ui->btn_refreshOffer, &QPushButton::clicked, this, [this]{ offerList->clear(); + ui->meta_label->setText("Updating offer book this may take a bit, if no offers appear after a while try refreshing again"); + auto m_instance = Config::instance(); QStringList pointList = m_instance->get(Config::rendezVous).toStringList(); for(QString point :pointList) @@ -88,129 +80,40 @@ AtomicWidget::AtomicWidget(QWidget *parent) */ }); - QTimer::singleShot(1, [this]{ - this->skinChanged(); + connect(ui->btn_addRendezvous, &QPushButton::clicked, this, [this]{ + //offerList = new QList>; + bool ok; + QString text = QInputDialog::getText(this, tr("Add new rendezvous point"), + tr("p2p multi address of rendezvous point"), QLineEdit::Normal, + "", &ok); + if (ok && !text.isEmpty()) { + QStringList copy = Config::instance()->get(Config::rendezVous).toStringList(); + copy.append(text); + Config::instance()->set(Config::rendezVous,copy); + } }); - m_statusTimer.start(5000); - connect(&m_statusTimer, &QTimer::timeout, this, &AtomicWidget::updateStatus); - QPixmap warningIcon = QPixmap(":/assets/images/warning.png"); - ui->icon_warning->setPixmap(warningIcon.scaledToWidth(32, Qt::SmoothTransformation)); - - this->updateStatus(); -} - -void AtomicWidget::convert(bool reverse) { - if (!m_comboBoxInit) - return; - - auto lineFrom = reverse ? ui->lineTo : ui->lineFrom; - auto lineTo = reverse ? ui->lineFrom : ui->lineTo; - - auto comboFrom = reverse ? ui->comboAtomicTo : ui->comboAtomicFrom; - auto comboTo = reverse ? ui->comboAtomicFrom : ui->comboAtomicTo; - - QString symbolFrom = comboFrom->itemText(comboFrom->currentIndex()); - QString symbolTo = comboTo->itemText(comboTo->currentIndex()); - - if (symbolFrom == symbolTo) { - lineTo->setText(lineFrom->text()); - } - - QString amountStr = lineFrom->text(); - double amount = amountStr.toDouble(); - double result = appData()->prices.convert(symbolFrom, symbolTo, amount); - - int precision = 10; - if (appData()->prices.rates.contains(symbolTo)) - precision = 2; - - lineTo->setText(QString::number(result, 'f', precision)); -} - -void AtomicWidget::onPricesReceived() { - if (m_comboBoxInit) - return; - - QList cryptoKeys = appData()->prices.markets.keys(); - QList fiatKeys = appData()->prices.rates.keys(); - if (cryptoKeys.empty() || fiatKeys.empty()) - return; - ui->btn_configure->setEnabled(true); - this->initComboBox(); - m_comboBoxInit = true; this->updateStatus(); } -void AtomicWidget::initComboBox() { - QList cryptoKeys = appData()->prices.markets.keys(); - QList fiatKeys = appData()->prices.rates.keys(); - - QStringList enabledCrypto = conf()->get(Config::cryptoSymbols).toStringList(); - QStringList filteredCryptoKeys; - for (const auto& symbol : cryptoKeys) { - if (enabledCrypto.contains(symbol)) { - filteredCryptoKeys.append(symbol); - } - } - - QStringList enabledFiat = conf()->get(Config::fiatSymbols).toStringList(); - auto preferredFiat = conf()->get(Config::preferredFiatCurrency).toString(); - if (!enabledFiat.contains(preferredFiat) && fiatKeys.contains(preferredFiat)) { - enabledFiat.append(preferredFiat); - conf()->set(Config::fiatSymbols, enabledFiat); - } - QStringList filteredFiatKeys; - for (const auto &symbol : fiatKeys) { - if (enabledFiat.contains(symbol)) { - filteredFiatKeys.append(symbol); - } - } - this->setupComboBox(ui->comboAtomicFrom, filteredCryptoKeys, filteredFiatKeys); - this->setupComboBox(ui->comboAtomicTo, filteredCryptoKeys, filteredFiatKeys); - - ui->comboAtomicFrom->setCurrentIndex(ui->comboAtomicFrom->findText("XMR")); - - if (!preferredFiat.isEmpty()) { - ui->comboAtomicTo->setCurrentIndex(ui->comboAtomicTo->findText(preferredFiat)); - } else { - ui->comboAtomicTo->setCurrentIndex(ui->comboAtomicTo->findText("USD")); - } -} void AtomicWidget::skinChanged() { - ui->imageExchange->setMode(ColorScheme::hasDarkBackground(this)); } void AtomicWidget::showAtomicConfigureDialog() { AtomicConfigDialog dialog{this}; if (dialog.exec() == QDialog::Accepted) { - this->initComboBox(); + } } -void AtomicWidget::setupComboBox(QComboBox *comboBox, const QStringList &crypto, const QStringList &fiat) { - comboBox->clear(); - comboBox->addItems(crypto); - comboBox->insertSeparator(comboBox->count()); - comboBox->addItems(fiat); -} + void AtomicWidget::updateStatus() { - if (!m_comboBoxInit) { - ui->label_warning->setText("Waiting on exchange data."); - ui->frame_warning->show(); - } - else if (websocketNotifier()->stale(10)) { - ui->label_warning->setText("No new exchange rates received for over 10 minutes."); - ui->frame_warning->show(); - } - else { - ui->frame_warning->hide(); - } + } void AtomicWidget::list(QString rendezvous) { @@ -248,18 +151,27 @@ void AtomicWidget::list(QString rendezvous) { qDebug() << "status contained"; parsedLine = QJsonDocument::fromJson(line.toLocal8Bit(), &parseError ); if (parsedLine["fields"]["status"].toString().contains("Online")){ - OfferEntry entry = {parsedLine["fields"]["price"].toDouble(),parsedLine["fields"]["min_quantity"].toDouble(),parsedLine["fields"]["max_quantity"].toDouble(), parsedLine["fields"]["address"].toString()}; - offerList->append(QSharedPointer(&entry)); - qDebug() << &entry; + bool skip = false; + auto entry = new OfferEntry(parsedLine["fields"]["price"].toString().split( ' ')[0].toDouble(),parsedLine["fields"]["min_quantity"].toString().split(' ')[0].toDouble(),parsedLine["fields"]["max_quantity"].toString().split(' ')[0].toDouble(), parsedLine["fields"]["address"].toString()); + for(auto post : *offerList){ + if(std::equal(entry->address.begin(), entry->address.end(),post->address.begin(),post->address.end())) + skip = true; + } + if (!skip) { + ui->meta_label->setText("Updated offer book"); + offerList->append(QSharedPointer(entry)); + } + qDebug() << entry; } } qDebug() << "next line"; } qDebug() << "exits fine"; swap->close(); + o_model->updateOffers(*offerList); return list; }); - swap->start("/home/dev/.config/feather/swap", arguments); + swap->start(m_instance->get(Config::swapPath).toString(), arguments); //swap->waitForFinished(120000); diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h index aaf1b9bb..8cf353d3 100644 --- a/src/plugins/atomic/AtomicWidget.h +++ b/src/plugins/atomic/AtomicWidget.h @@ -29,13 +29,9 @@ public slots: void skinChanged(); private slots: - void initComboBox(); void showAtomicConfigureDialog(); - void onPricesReceived(); private: - void convert(bool reverse); - void setupComboBox(QComboBox *comboBox, const QStringList &crypto, const QStringList &fiat); void updateStatus(); QScopedPointer ui; diff --git a/src/plugins/atomic/AtomicWidget.ui b/src/plugins/atomic/AtomicWidget.ui index aa5a21e6..3bfd080d 100644 --- a/src/plugins/atomic/AtomicWidget.ui +++ b/src/plugins/atomic/AtomicWidget.ui @@ -15,124 +15,76 @@ - - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - - - - - - - 0 - 0 - - - - icon - - - - - - - Qt::Orientation::Horizontal - - - QSizePolicy::Policy::Maximum - - - - 10 - 20 - - - - - - - - Warning text - - - - - + - - - 18 - - - - - 0 - - - 0 - + + + - - - - 0 - 0 - - - - true - - - From... + + + BTC change address - + - - - - exchange image - - - - - - - 0 - + + - - - - 0 - 0 - - - - false - - - To... + + + XMR receive address - + - + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Swap + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + @@ -150,6 +102,13 @@ + + + + No swap tool configured, click configure to set up + + + @@ -174,13 +133,6 @@ - - - DoublePixmapLabel - QLabel -
components.h
-
-
From a84ff2ef037ac70068fc3b41480e4a240434c17d Mon Sep 17 00:00:00 2001 From: twiddle Date: Sun, 2 Jun 2024 21:13:56 -0400 Subject: [PATCH 08/26] Bump to swap tool v 0.13.0 --- src/plugins/atomic/AtomicConfigDialog.cpp | 6 +- src/plugins/atomic/AtomicWidget.cpp | 76 ++++++++++++++++------- src/plugins/atomic/AtomicWidget.h | 1 + 3 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp index 6b36de53..9ef469a2 100644 --- a/src/plugins/atomic/AtomicConfigDialog.cpp +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -60,11 +60,11 @@ void AtomicConfigDialog::downloadBinary() { auto operatingSystem = Config::instance()->get(Config::operatingSystem).toString().toStdString(); if(strcmp("WIN",operatingSystem.c_str()) == 0) { // HARD CODED DOWNload URL CHANGE IF PROBLEMS - url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.12.3/swap_0.12.3_Windows_x86_64.zip"); + url = QString("https://github.com/comit-network/xmr-btc-swap/releases/tag/0.13.0/swap_0.13.0_Windows_x86_64.zip"); } else if (strcmp("LINUX",operatingSystem.c_str())==0){ - url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.12.3/swap_0.12.3_Linux_x86_64.tar"); + url = QString("https://github.com/comit-network/xmr-btc-swap/releases/tag/0.13.0/swap_0.13.0_Linux_x86_64.tar"); } else { - url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.12.3/swap_0.12.3_Darwin_x86_64.tar"); + url = QString("https://github.com/comit-network/xmr-btc-swap/releases/tag/0.13.0/swap_0.13.0_Darwin_x86_64.tar"); } archive = network->get(this, url); diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 908ce95f..0cf577b6 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -53,31 +53,20 @@ AtomicWidget::AtomicWidget(QWidget *parent) QStringList pointList = m_instance->get(Config::rendezVous).toStringList(); for(QString point :pointList) AtomicWidget::list(point); + }); - /* - QList> tempList; - for (int i=0; iappend(offer); - } - } - qDebug() << "done"; - - /*for(auto offer: AtomicWidget::list(Config::instance()->get(Config::rendezVous).toStringList()[0])){ - offerList->append(offer); + connect(ui->btn_swap, &QPushButton::clicked, this, [this]{ + auto rows = ui->offerBookTable->selectionModel()->selectedRows(); + if (rows.size() < 1){ + ui->meta_label->setText("You must select an offer to use for swap, refresh if there aren't any"); + } else { + QModelIndex index = rows.at(0); + QString seller = index.sibling(index.row(), 3).data().toString(); + //Add proper error checking on ui input after rest of swap is implemented + QString btcChange = ui->change_address->text(); + QString xmrReceive = ui->xmr_address->text(); + runSwap(seller,btcChange, xmrReceive); } - o_model->updateOffers(*offerList); - - */ }); connect(ui->btn_addRendezvous, &QPushButton::clicked, this, [this]{ @@ -116,6 +105,45 @@ void AtomicWidget::updateStatus() { } +void AtomicWidget::runSwap(QString seller, QString btcChange, QString xmrReceive) { + qDebug() << "starting swap"; + QStringList arguments; + auto m_instance = Config::instance(); + arguments << "--data-base-dir"; + arguments << Config::defaultConfigDir().absolutePath(); + // Remove after testing + //arguments << "--testnet"; + arguments << "-j"; + arguments << "buy-xmr"; + arguments << "--change-address"; + arguments << btcChange; + arguments << "--receive-address"; + arguments << xmrReceive; + arguments << "--seller"; + arguments << seller; + arguments << "--tor-socks5-port"; + arguments << m_instance->get(Config::socks5Port).toString(); + + auto *swap = new QProcess(); + swap->setReadChannel(QProcess::StandardError); + connect(swap, &QProcess::readyReadStandardError,this, [this, swap] { + while(swap->canReadLine()){ + QJsonParseError err; + QJsonDocument line = QJsonDocument::fromJson(swap->readLine(), &err); + qDebug() << line; + if (line["fields"]["message"].toString().contains("Connected to Alice")){ + qDebug() << "Succesfully connected"; + } else if (!line["fields"]["deposit_address"].toString().isEmpty()){ + qDebug() << "Deposit to btc to segwit address"; + } else if () + //Insert line conditionals here + } + }); + swap->start(m_instance->get(Config::swapPath).toString(),arguments); + qDebug() << "process started"; + +} + void AtomicWidget::list(QString rendezvous) { QStringList arguments; QList> list; @@ -131,7 +159,7 @@ void AtomicWidget::list(QString rendezvous) { arguments << "--rendezvous-point"; arguments << rendezvous; auto *swap = new QProcess(); - swap->setReadChannel(QProcess::StandardOutput); + swap->setReadChannel(QProcess::StandardError); //swap->start(m_instance->get(Config::swapPath).toString(), arguments); connect(swap, &QProcess::finished, this, [this, swap]{ QJsonDocument parsedLine; diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h index 8cf353d3..37a21d7b 100644 --- a/src/plugins/atomic/AtomicWidget.h +++ b/src/plugins/atomic/AtomicWidget.h @@ -30,6 +30,7 @@ public slots: private slots: void showAtomicConfigureDialog(); + void runSwap(QString seller, QString btcChange, QString xmrReceive); private: void updateStatus(); From cba9eabb4b5140e8115ebd7857b60113b12dcfef Mon Sep 17 00:00:00 2001 From: twiddle Date: Tue, 11 Jun 2024 21:08:47 -0400 Subject: [PATCH 09/26] Atomic Swap UI to show progress of swap started Bump to swap tool v 0.13.1 ** Fix to kill swap process & monero-wallet-rpc process on shutdown --- src/ReceiveWidget.cpp | 1 - src/plugins/atomic/AtomicConfigDialog.cpp | 6 +-- src/plugins/atomic/AtomicSwap.cpp | 28 ++++++++++++ src/plugins/atomic/AtomicSwap.h | 32 +++++++++++++ src/plugins/atomic/AtomicSwap.ui | 55 +++++++++++++++++++++++ src/plugins/atomic/AtomicWidget.cpp | 47 +++++++++++++++---- src/plugins/atomic/AtomicWidget.h | 8 ++++ src/plugins/atomic/AtomicWidget.ui | 12 ++++- src/utils/config.cpp | 5 ++- 9 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 src/plugins/atomic/AtomicSwap.cpp create mode 100644 src/plugins/atomic/AtomicSwap.h create mode 100644 src/plugins/atomic/AtomicSwap.ui diff --git a/src/ReceiveWidget.cpp b/src/ReceiveWidget.cpp index c57e5c9f..2bc77dbe 100644 --- a/src/ReceiveWidget.cpp +++ b/src/ReceiveWidget.cpp @@ -247,7 +247,6 @@ void ReceiveWidget::updateQrCode(){ void ReceiveWidget::showQrCodeDialog() { SubaddressRow* row = this->currentEntry(); - if (!row) return; QString address = this->getAddress(row->getRow()); QrCode qr(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH); diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp index 9ef469a2..c4fc4124 100644 --- a/src/plugins/atomic/AtomicConfigDialog.cpp +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -60,11 +60,11 @@ void AtomicConfigDialog::downloadBinary() { auto operatingSystem = Config::instance()->get(Config::operatingSystem).toString().toStdString(); if(strcmp("WIN",operatingSystem.c_str()) == 0) { // HARD CODED DOWNload URL CHANGE IF PROBLEMS - url = QString("https://github.com/comit-network/xmr-btc-swap/releases/tag/0.13.0/swap_0.13.0_Windows_x86_64.zip"); + url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.13.1/swap_0.13.1_Windows_x86_64.zip"); } else if (strcmp("LINUX",operatingSystem.c_str())==0){ - url = QString("https://github.com/comit-network/xmr-btc-swap/releases/tag/0.13.0/swap_0.13.0_Linux_x86_64.tar"); + url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.13.1/swap_0.13.1_Linux_x86_64.tar"); } else { - url = QString("https://github.com/comit-network/xmr-btc-swap/releases/tag/0.13.0/swap_0.13.0_Darwin_x86_64.tar"); + url = QString("https://github.com/comit-network/xmr-btc-swap/releases/download/0.13.1/swap_0.13.1_Linux_x86_64.tar"); } archive = network->get(this, url); diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp new file mode 100644 index 00000000..f72fdf8b --- /dev/null +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -0,0 +1,28 @@ +// +// Created by dev on 6/11/24. +// + +// You may need to build the project (run Qt uic code generator) to get "ui_AtomicSwap.h" resolved + +#include "AtomicSwap.h" +#include "ui_AtomicSwap.h" + + + +AtomicSwap::AtomicSwap(QWidget *parent) : + QDialog(parent), ui(new Ui::AtomicSwap) { + ui->setupUi(this); +} + + +AtomicSwap::~AtomicSwap() { + delete ui; +} +void AtomicSwap::logLine(QString line){ + ui->debug_log->setText(ui->debug_log->toPlainText().append(QTime::currentTime().toString() + ":" + line)); + this->update(); +} +void AtomicSwap::updateStatus(QString status){ + ui->label_status->setText(status); + this->update(); +} \ No newline at end of file diff --git a/src/plugins/atomic/AtomicSwap.h b/src/plugins/atomic/AtomicSwap.h new file mode 100644 index 00000000..286c1eb9 --- /dev/null +++ b/src/plugins/atomic/AtomicSwap.h @@ -0,0 +1,32 @@ +// +// Created by dev on 6/11/24. +// + +#ifndef FEATHER_ATOMICSWAP_H +#define FEATHER_ATOMICSWAP_H + +#include +#include + + +QT_BEGIN_NAMESPACE +namespace Ui { class AtomicSwap; } +QT_END_NAMESPACE + +class AtomicSwap : public QDialog { +Q_OBJECT + +public: + explicit AtomicSwap(QWidget *parent = nullptr); + void updateStatus(QString status); + void logLine(QString line); + ~AtomicSwap() override; + +private: + Ui::AtomicSwap *ui; + + +}; + + +#endif //FEATHER_ATOMICSWAP_H diff --git a/src/plugins/atomic/AtomicSwap.ui b/src/plugins/atomic/AtomicSwap.ui new file mode 100644 index 00000000..4519c2d9 --- /dev/null +++ b/src/plugins/atomic/AtomicSwap.ui @@ -0,0 +1,55 @@ + + + AtomicSwap + + + + 0 + 0 + 400 + 300 + + + + AtomicSwap + + + + + 0 + 0 + 361 + 271 + + + + + + + Connected to peer, swap starting + + + + + + + + + + + + Debug Log + + + + + + + + + + + + + + diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 0cf577b6..341591b2 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -12,14 +12,17 @@ #include "OfferModel.h" #include "utils/AppData.h" #include "utils/ColorScheme.h" -#include "utils/config.h" #include "utils/WebsocketNotifier.h" +#include "dialog/QrCodeDialog.h" AtomicWidget::AtomicWidget(QWidget *parent) : QWidget(parent) , ui(new Ui::AtomicWidget) + , m_instance(Config::instance()) , o_model(new OfferModel(this)) , offerList(new QList>()) + , swapDialog(new AtomicSwap(this)) + , procList(new QList>()) { ui->setupUi(this); @@ -49,7 +52,6 @@ AtomicWidget::AtomicWidget(QWidget *parent) ui->meta_label->setText("Updating offer book this may take a bit, if no offers appear after a while try refreshing again"); - auto m_instance = Config::instance(); QStringList pointList = m_instance->get(Config::rendezVous).toStringList(); for(QString point :pointList) AtomicWidget::list(point); @@ -65,6 +67,7 @@ AtomicWidget::AtomicWidget(QWidget *parent) //Add proper error checking on ui input after rest of swap is implemented QString btcChange = ui->change_address->text(); QString xmrReceive = ui->xmr_address->text(); + showAtomicSwapDialog(); runSwap(seller,btcChange, xmrReceive); } }); @@ -99,6 +102,9 @@ void AtomicWidget::showAtomicConfigureDialog() { } } +void AtomicWidget::showAtomicSwapDialog() { + swapDialog->show(); +} void AtomicWidget::updateStatus() { @@ -108,7 +114,6 @@ void AtomicWidget::updateStatus() { void AtomicWidget::runSwap(QString seller, QString btcChange, QString xmrReceive) { qDebug() << "starting swap"; QStringList arguments; - auto m_instance = Config::instance(); arguments << "--data-base-dir"; arguments << Config::defaultConfigDir().absolutePath(); // Remove after testing @@ -125,17 +130,28 @@ void AtomicWidget::runSwap(QString seller, QString btcChange, QString xmrReceive arguments << m_instance->get(Config::socks5Port).toString(); auto *swap = new QProcess(); + procList->append(QSharedPointer(swap)); + swap->setReadChannel(QProcess::StandardError); connect(swap, &QProcess::readyReadStandardError,this, [this, swap] { while(swap->canReadLine()){ QJsonParseError err; - QJsonDocument line = QJsonDocument::fromJson(swap->readLine(), &err); - qDebug() << line; + const QByteArray& rawline = swap->readLine(); + QJsonDocument line = QJsonDocument::fromJson(rawline, &err); + qDebug() << rawline; if (line["fields"]["message"].toString().contains("Connected to Alice")){ qDebug() << "Succesfully connected"; + swapDialog->logLine(line["fields"].toString()); } else if (!line["fields"]["deposit_address"].toString().isEmpty()){ qDebug() << "Deposit to btc to segwit address"; - } else if () + QString address = line["fields"]["deposit_address"].toString(); + QrCode qrc(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH); + swapDialog->updateStatus("Add money to this address\n" + address); + QrCodeDialog dialog(this, &qrc, "Deposit BTC to this address"); + dialog.show(); + } else{ + + } //Insert line conditionals here } }); @@ -147,7 +163,6 @@ void AtomicWidget::runSwap(QString seller, QString btcChange, QString xmrReceive void AtomicWidget::list(QString rendezvous) { QStringList arguments; QList> list; - auto m_instance = Config::instance(); arguments << "--data-base-dir"; arguments << Config::defaultConfigDir().absolutePath(); // Remove after testing @@ -159,6 +174,7 @@ void AtomicWidget::list(QString rendezvous) { arguments << "--rendezvous-point"; arguments << rendezvous; auto *swap = new QProcess(); + procList->append(QSharedPointer(swap)); swap->setReadChannel(QProcess::StandardError); //swap->start(m_instance->get(Config::swapPath).toString(), arguments); connect(swap, &QProcess::finished, this, [this, swap]{ @@ -206,4 +222,19 @@ void AtomicWidget::list(QString rendezvous) { } -AtomicWidget::~AtomicWidget() = default; \ No newline at end of file +AtomicWidget::~AtomicWidget() { + qDebug()<< "EXiting widget!!"; + delete o_model; + delete offerList; + for (auto proc : *procList){ + if(!proc->atEnd()) + proc->terminate(); + } + if(QString::compare("WINDOWS",m_instance->get(Config::operatingSystem).toString()) != 0) { + qDebug() << "Closing monero-wallet-rpc"; + (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + + "/mainnet/monero/monero-wallet-rpc"}); + } + delete m_instance; + delete procList; +}; \ No newline at end of file diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h index 37a21d7b..d33cb1f2 100644 --- a/src/plugins/atomic/AtomicWidget.h +++ b/src/plugins/atomic/AtomicWidget.h @@ -11,6 +11,8 @@ #include #include "OfferModel.h" +#include "AtomicSwap.h" +#include "config.h" namespace Ui { class AtomicWidget; @@ -40,6 +42,12 @@ private slots: QTimer m_statusTimer; OfferModel *o_model; QList> *offerList; + AtomicSwap *swapDialog; + + void showAtomicSwapDialog(); + + QList> *procList; + Config *m_instance; }; #endif // FEATHER_ATOMICWIDGET_H diff --git a/src/plugins/atomic/AtomicWidget.ui b/src/plugins/atomic/AtomicWidget.ui index 3bfd080d..90ef700a 100644 --- a/src/plugins/atomic/AtomicWidget.ui +++ b/src/plugins/atomic/AtomicWidget.ui @@ -29,7 +29,11 @@
- + + + bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq + +
@@ -43,7 +47,11 @@
- + + + 888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H + + diff --git a/src/utils/config.cpp b/src/utils/config.cpp index e7878dce..53582696 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -142,9 +142,10 @@ static const QHash configStrings = { {Config::tickersShowFiatBalance, {QS("tickersShowFiatBalance"), true}}, // Atomic - {Config::rendezVous, {QS("rendezVous"), QStringList{"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", - "/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs", + {Config::rendezVous, {QS("rendezVous"), QStringList{"/dns4/atomicswaps.majesticbank.at/tcp/8888/p2p/12D3KooWKJUwP45K7fLbwGY1VM5V3U7LseU8EwJiAozUFrq5ihoF", "/dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o", + "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", + "/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs", "/dns4/swap.sethforprivacy.com/tcp/8888/p2p/12D3KooWCULyZKuV9YEkb6BX8FuwajdvktSzmMg4U5ZX2uYZjHeu"}}}, {Config::swapPath, {QS("swapPath"), ""}}, {Config::operatingSystem, {QS("operatingSystem"), OS}}, From fd6b004192743be17d803af47af05a22b1e8a2a1 Mon Sep 17 00:00:00 2001 From: twiddle Date: Fri, 21 Jun 2024 18:06:07 -0400 Subject: [PATCH 10/26] Update README to link to xmr-btc-swap-json-tests repo. To allow for easier testing and development. --- src/plugins/atomic/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/atomic/README.md b/src/plugins/atomic/README.md index 00ebe754..ad464183 100644 --- a/src/plugins/atomic/README.md +++ b/src/plugins/atomic/README.md @@ -6,6 +6,10 @@ Built in xmr-btc atomic swap ## Hacking sudo apt install libarchive-dev +It may also be helpful to download my fork of the comit network's xmr-btc swap tool as it has tests set to ouput JSON and scripts that allow for easy simulation of running swap (filters to Bob POV & removes wrappers on normal output) + +[xmr-btc-swap-json-tests](https://github.com/BrandyJSon/xmr-btc-swap-json-tests/tree/master) + Functions used to control swap (AtomicPlugin.cpp) withdraw(btcaddress) - withdraw btc From b9fde514af6077ce68ebff8bfb483f73a47e3cb3 Mon Sep 17 00:00:00 2001 From: twiddle Date: Thu, 27 Jun 2024 11:49:09 -0400 Subject: [PATCH 11/26] Update README again. Swap tool has not been set up to do btc testnet4 -> xmr stagenet atomic swaps. Making testing much more practical. --- src/plugins/atomic/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/atomic/README.md b/src/plugins/atomic/README.md index ad464183..fc63fabc 100644 --- a/src/plugins/atomic/README.md +++ b/src/plugins/atomic/README.md @@ -6,7 +6,7 @@ Built in xmr-btc atomic swap ## Hacking sudo apt install libarchive-dev -It may also be helpful to download my fork of the comit network's xmr-btc swap tool as it has tests set to ouput JSON and scripts that allow for easy simulation of running swap (filters to Bob POV & removes wrappers on normal output) +It may also be helpful to download my fork of the comit network's xmr-btc swap tool as it has everything you need to use btc's testnet4 for swaps. Testnet4 btc is much easier to acquire so it makes testing much more practical. There are scripts to run the electrum server just make sure you have docker-compose installed and build the electrumx testnet4 fork before trying to run the scripts. [xmr-btc-swap-json-tests](https://github.com/BrandyJSon/xmr-btc-swap-json-tests/tree/master) From 513d83229f8f3adfe76189390ac3351cdd1b990c Mon Sep 17 00:00:00 2001 From: twiddle Date: Mon, 8 Jul 2024 14:49:24 -0400 Subject: [PATCH 12/26] Added AtomicFundDialog ui to make depositing btc for swap easy. Updated AtomicSwap functionality to clean swap and monero-wallet-rpc. Can run swap successfully if nothing goes wrong. TODO: --- src/plugins/atomic/AtomicFundDialog.cpp | 57 ++++++++++++ src/plugins/atomic/AtomicFundDialog.h | 37 ++++++++ src/plugins/atomic/AtomicFundDialog.ui | 111 ++++++++++++++++++++++++ src/plugins/atomic/AtomicSwap.cpp | 30 ++++++- src/plugins/atomic/AtomicSwap.h | 9 +- src/plugins/atomic/AtomicSwap.ui | 107 +++++++++++++++++++---- src/plugins/atomic/AtomicWidget.cpp | 49 ++++++++--- src/plugins/atomic/AtomicWidget.h | 4 +- 8 files changed, 364 insertions(+), 40 deletions(-) create mode 100644 src/plugins/atomic/AtomicFundDialog.cpp create mode 100644 src/plugins/atomic/AtomicFundDialog.h create mode 100644 src/plugins/atomic/AtomicFundDialog.ui diff --git a/src/plugins/atomic/AtomicFundDialog.cpp b/src/plugins/atomic/AtomicFundDialog.cpp new file mode 100644 index 00000000..961a537c --- /dev/null +++ b/src/plugins/atomic/AtomicFundDialog.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "AtomicFundDialog.h" +#include "ui_AtomicFundDialog.h" + +#include +#include +#include + +AtomicFundDialog::AtomicFundDialog(QWidget *parent, QrCode *qrCode, const QString &title, const QString &btc_address) + : WindowModalDialog(parent) + , ui(new Ui::AtomicFundDialog) + , address(btc_address) +{ + ui->setupUi(this); + this->setWindowTitle(title); + ui->qrWidget->setQrCode(qrCode); + m_pixmap = qrCode->toPixmap(1).scaled(500, 500, Qt::KeepAspectRatio); + + connect(ui->btn_CopyImage, &QPushButton::clicked, this, &AtomicFundDialog::copyImage); + connect(ui->btn_Save, &QPushButton::clicked, this, &AtomicFundDialog::saveImage); + connect(ui->btn_Close, &QPushButton::clicked, [this](){ + emit cleanProcs(); + accept(); + }); + + this->resize(500, 500); +} + +void AtomicFundDialog::copyImage() { + QApplication::clipboard()->setPixmap(m_pixmap); + QMessageBox::information(this, "Information", "QR code copied to clipboard"); +} + +void AtomicFundDialog::saveImage() { + QString filename = QFileDialog::getSaveFileName(this, "Select where to save file", QDir::current().filePath("qrcode.png")); + if (filename.isEmpty()) { + return; + } + + QFile file(filename); + file.open(QIODevice::WriteOnly); + m_pixmap.save(&file, "PNG"); + QMessageBox::information(this, "Information", "QR code saved to file"); +} + +void AtomicFundDialog::copyAddress(){ + QApplication::clipboard()->setText(address); + QMessageBox::information(this, "Information", "BTC deposit address copied to clipboard"); +} + + + +AtomicFundDialog::~AtomicFundDialog() { + emit cleanProcs(); +} \ No newline at end of file diff --git a/src/plugins/atomic/AtomicFundDialog.h b/src/plugins/atomic/AtomicFundDialog.h new file mode 100644 index 00000000..467664e6 --- /dev/null +++ b/src/plugins/atomic/AtomicFundDialog.h @@ -0,0 +1,37 @@ +// +// Created by dev on 7/8/24. +// + +#ifndef FEATHER_ATOMICFUNDDIALOG_H +#define FEATHER_ATOMICFUNDDIALOG_H + +#include +#include "components.h" +#include "qrcode/QrCode.h" +#include "widgets/QrCodeWidget.h" + + +namespace Ui { + class AtomicFundDialog; +} + + +class AtomicFundDialog : public WindowModalDialog { + Q_OBJECT + +public: + explicit AtomicFundDialog(QWidget *parent, QrCode *qrCode, const QString &title = "Qr Code", const QString &btc_address = "Error Restart swap"); + ~AtomicFundDialog() override; +signals: + void cleanProcs(); +private: + void copyImage(); + void saveImage(); + void copyAddress(); + QScopedPointer ui; + QPixmap m_pixmap; + QString address; +}; + + +#endif //FEATHER_ATOMICFUNDDIALOG_H diff --git a/src/plugins/atomic/AtomicFundDialog.ui b/src/plugins/atomic/AtomicFundDialog.ui new file mode 100644 index 00000000..29cfc439 --- /dev/null +++ b/src/plugins/atomic/AtomicFundDialog.ui @@ -0,0 +1,111 @@ + + + AtomicFundDialog + + + + 0 + 0 + 520 + 446 + + + + Dialog + + + + + + + 0 + 0 + + + + + 150 + 150 + + + + + + + + QLayout::SetDefaultConstraint + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy Address + + + false + + + true + + + + + + + Copy Image + + + false + + + + + + + Save + + + false + + + + + + + Cancel + + + false + + + true + + + + + + + + + + QrCodeWidget + QWidget +
widgets/QrCodeWidget.h
+ 1 +
+
+ + +
\ No newline at end of file diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp index f72fdf8b..83afe1b0 100644 --- a/src/plugins/atomic/AtomicSwap.cpp +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -6,23 +6,45 @@ #include "AtomicSwap.h" #include "ui_AtomicSwap.h" - +#include "AtomicWidget.h" AtomicSwap::AtomicSwap(QWidget *parent) : - QDialog(parent), ui(new Ui::AtomicSwap) { + WindowModalDialog(parent), ui(new Ui::AtomicSwap) { ui->setupUi(this); + //ui->debug_log->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); + ui->label_status->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); + this->setContentsMargins(3,3,3,3); + this->adjustSize(); } AtomicSwap::~AtomicSwap() { delete ui; + emit cleanProcs(); } void AtomicSwap::logLine(QString line){ - ui->debug_log->setText(ui->debug_log->toPlainText().append(QTime::currentTime().toString() + ":" + line)); + //ui->debug_log->setText(ui->debug_log->toPlainText().append(QTime::currentTime().toString() + ":" + line)); + this->update(); } void AtomicSwap::updateStatus(QString status){ ui->label_status->setText(status); this->update(); -} \ No newline at end of file +} + +void AtomicSwap::updateXMRConf(int confs) { + ui->label_xmr_cons->setText(QString::number(confs)); + this->update(); +} + +void AtomicSwap::updateBTCConf(int confs) { + ui->label_btc_cons->setText(QString::number(confs)); + this->update(); +} + +void AtomicSwap::setTitle(QString title) { + this->setWindowTitle(title); + this->update(); +} + diff --git a/src/plugins/atomic/AtomicSwap.h b/src/plugins/atomic/AtomicSwap.h index 286c1eb9..b32ee1c4 100644 --- a/src/plugins/atomic/AtomicSwap.h +++ b/src/plugins/atomic/AtomicSwap.h @@ -7,13 +7,14 @@ #include #include +#include "components.h" QT_BEGIN_NAMESPACE namespace Ui { class AtomicSwap; } QT_END_NAMESPACE -class AtomicSwap : public QDialog { +class AtomicSwap : public WindowModalDialog { Q_OBJECT public: @@ -21,7 +22,11 @@ Q_OBJECT void updateStatus(QString status); void logLine(QString line); ~AtomicSwap() override; - + void updateBTCConf(int confs); + void updateXMRConf(int confs); + void setTitle(QString title); +signals: + void cleanProcs(); private: Ui::AtomicSwap *ui; diff --git a/src/plugins/atomic/AtomicSwap.ui b/src/plugins/atomic/AtomicSwap.ui index 4519c2d9..9a1ae4ec 100644 --- a/src/plugins/atomic/AtomicSwap.ui +++ b/src/plugins/atomic/AtomicSwap.ui @@ -6,44 +6,113 @@ 0 0 - 400 - 300 + 534 + 200 AtomicSwap - + - 0 - 0 - 361 - 271 + 30 + 10 + 471 + 151 - - - Connected to peer, swap starting - - - - - + + + + + Btc deposited to wallet, begining swap + + + + - + - + - Debug Log + Cancel - + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + border-color: rgb(222, 221, 218); + + + + QFrame::Shape::Box + + + + + + + + + On chain Confimations + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + BTC : + + + + + + + 0 + + + + + + + XMR : + + + + + + + 0 + + + + + + + diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 341591b2..03c4d481 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -13,7 +13,7 @@ #include "utils/AppData.h" #include "utils/ColorScheme.h" #include "utils/WebsocketNotifier.h" -#include "dialog/QrCodeDialog.h" +#include "AtomicFundDialog.h" AtomicWidget::AtomicWidget(QWidget *parent) : QWidget(parent) @@ -67,7 +67,6 @@ AtomicWidget::AtomicWidget(QWidget *parent) //Add proper error checking on ui input after rest of swap is implemented QString btcChange = ui->change_address->text(); QString xmrReceive = ui->xmr_address->text(); - showAtomicSwapDialog(); runSwap(seller,btcChange, xmrReceive); } }); @@ -84,6 +83,7 @@ AtomicWidget::AtomicWidget(QWidget *parent) Config::instance()->set(Config::rendezVous,copy); } }); + connect(swapDialog,&AtomicSwap::cleanProcs, this, [this]{clean();}); this->updateStatus(); @@ -106,7 +106,6 @@ void AtomicWidget::showAtomicSwapDialog() { swapDialog->show(); } - void AtomicWidget::updateStatus() { } @@ -121,14 +120,26 @@ void AtomicWidget::runSwap(QString seller, QString btcChange, QString xmrReceive arguments << "-j"; arguments << "buy-xmr"; arguments << "--change-address"; + //arguments << "tb1qzndh6u8qgl2ee4k4gl9erg947g67hyx03vvgen"; arguments << btcChange; arguments << "--receive-address"; + //arguments << "78YnzFTp3UUMgtKuAJCP2STcbxRZPDPveJ5YGgfg5doiPahS9suWF1r3JhKqjM1McYBJvu8nhkXExGfXVkU6n5S6AXrg4KP"; arguments << xmrReceive; arguments << "--seller"; + //arguments << "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWQA4fXDYLNXgxPsVZmnR8kh2wwHUQnkH9e1Wjc8KyJ7p8"; + // Remove after testing + //arguments << "--electrum-rpc"; + //arguments << "tcp://127.0.0.1:50001"; + //arguments << "--bitcoin-target-block"; + //arguments << "8"; + //arguments << "--monero-daemon-address"; + //arguments << "http://127.0.0.1:38083"; + // Uncomment after testing arguments << seller; arguments << "--tor-socks5-port"; arguments << m_instance->get(Config::socks5Port).toString(); + auto *swap = new QProcess(); procList->append(QSharedPointer(swap)); @@ -146,15 +157,21 @@ void AtomicWidget::runSwap(QString seller, QString btcChange, QString xmrReceive qDebug() << "Deposit to btc to segwit address"; QString address = line["fields"]["deposit_address"].toString(); QrCode qrc(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH); - swapDialog->updateStatus("Add money to this address\n" + address); - QrCodeDialog dialog(this, &qrc, "Deposit BTC to this address"); - dialog.show(); - } else{ - + AtomicFundDialog dialog(qobject_cast(parent()), &qrc, "Deposit BTC to this address", address); + connect(&dialog,&AtomicFundDialog::cleanProcs, this, [this]{clean();}); + connect(this, &AtomicWidget::receivedBTC,&dialog, [this, &dialog]{ + disconnect(&dialog, SIGNAL(cleanProcs()), nullptr, nullptr); + dialog.close();}); + dialog.exec(); + } else if (line["fields"]["message"].toString().startsWith("Received Bitcoin")){ + emit receivedBTC(line["fields"]["new_balance"].toString().split(" ")[0].toDouble()); + qDebug() << "Spawn atomic swap progress dialog"; + showAtomicSwapDialog(); } //Insert line conditionals here } }); + swap->start(m_instance->get(Config::swapPath).toString(),arguments); qDebug() << "process started"; @@ -165,8 +182,6 @@ void AtomicWidget::list(QString rendezvous) { QList> list; arguments << "--data-base-dir"; arguments << Config::defaultConfigDir().absolutePath(); - // Remove after testing - //arguments << "--testnet"; arguments << "-j"; arguments << "list-sellers"; arguments << "--tor-socks5-port"; @@ -208,7 +223,6 @@ void AtomicWidget::list(QString rendezvous) { qDebug() << entry; } } - qDebug() << "next line"; } qDebug() << "exits fine"; swap->close(); @@ -226,6 +240,12 @@ AtomicWidget::~AtomicWidget() { qDebug()<< "EXiting widget!!"; delete o_model; delete offerList; + clean(); + delete m_instance; + delete procList; +} + +void AtomicWidget::clean() { for (auto proc : *procList){ if(!proc->atEnd()) proc->terminate(); @@ -234,7 +254,8 @@ AtomicWidget::~AtomicWidget() { qDebug() << "Closing monero-wallet-rpc"; (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + "/mainnet/monero/monero-wallet-rpc"}); + (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + + "/testnet/monero/monero-wallet-rpc"}); } - delete m_instance; - delete procList; -}; \ No newline at end of file +} + diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h index d33cb1f2..1b4bfb19 100644 --- a/src/plugins/atomic/AtomicWidget.h +++ b/src/plugins/atomic/AtomicWidget.h @@ -29,11 +29,13 @@ Q_OBJECT public slots: void skinChanged(); + void clean(); private slots: void showAtomicConfigureDialog(); void runSwap(QString seller, QString btcChange, QString xmrReceive); - +signals: + void receivedBTC(float new_amount); private: void updateStatus(); From 8744138b42261b49cc2aeaf7be6a49309ef13e19 Mon Sep 17 00:00:00 2001 From: twiddle Date: Mon, 8 Jul 2024 14:58:32 -0400 Subject: [PATCH 13/26] Added AtomicFundDialog ui to make depositing btc for swap easy. Updated AtomicSwap functionality to clean swap and monero-wallet-rpc. Can run swap successfully if nothing goes wrong. Cleaned some code TODO: 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, possibly add automatic cancel functionality --- src/plugins/atomic/AtomicWidget.cpp | 22 ++++++++++------------ src/plugins/atomic/AtomicWidget.h | 4 ++-- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 03c4d481..5a69383f 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -31,7 +31,6 @@ AtomicWidget::AtomicWidget(QWidget *parent) QString amount_rx = R"(^\d{0,8}[\.]\d{0,12}$)"; QRegularExpression rx; rx.setPattern(amount_rx); - QValidator *validator = new QRegularExpressionValidator(rx, this); ui->offerBookTable->setModel(o_model); ui->offerBookTable->setSortingEnabled(true); ui->offerBookTable->sortByColumn(0, Qt::SortOrder::AscendingOrder); @@ -53,7 +52,7 @@ AtomicWidget::AtomicWidget(QWidget *parent) ui->meta_label->setText("Updating offer book this may take a bit, if no offers appear after a while try refreshing again"); QStringList pointList = m_instance->get(Config::rendezVous).toStringList(); - for(QString point :pointList) + for(const QString& point :pointList) AtomicWidget::list(point); }); @@ -110,7 +109,7 @@ void AtomicWidget::updateStatus() { } -void AtomicWidget::runSwap(QString seller, QString btcChange, QString xmrReceive) { +void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, const QString& xmrReceive) { qDebug() << "starting swap"; QStringList arguments; arguments << "--data-base-dir"; @@ -151,7 +150,7 @@ void AtomicWidget::runSwap(QString seller, QString btcChange, QString xmrReceive QJsonDocument line = QJsonDocument::fromJson(rawline, &err); qDebug() << rawline; if (line["fields"]["message"].toString().contains("Connected to Alice")){ - qDebug() << "Succesfully connected"; + qDebug() << "Successfully connected"; swapDialog->logLine(line["fields"].toString()); } else if (!line["fields"]["deposit_address"].toString().isEmpty()){ qDebug() << "Deposit to btc to segwit address"; @@ -159,12 +158,12 @@ void AtomicWidget::runSwap(QString seller, QString btcChange, QString xmrReceive QrCode qrc(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH); AtomicFundDialog dialog(qobject_cast(parent()), &qrc, "Deposit BTC to this address", address); connect(&dialog,&AtomicFundDialog::cleanProcs, this, [this]{clean();}); - connect(this, &AtomicWidget::receivedBTC,&dialog, [this, &dialog]{ + connect(this, &AtomicWidget::receivedBTC,&dialog, [ &dialog]{ disconnect(&dialog, SIGNAL(cleanProcs()), nullptr, nullptr); dialog.close();}); dialog.exec(); } else if (line["fields"]["message"].toString().startsWith("Received Bitcoin")){ - emit receivedBTC(line["fields"]["new_balance"].toString().split(" ")[0].toDouble()); + emit receivedBTC(line["fields"]["new_balance"].toString().split(" ")[0].toFloat()); qDebug() << "Spawn atomic swap progress dialog"; showAtomicSwapDialog(); } @@ -177,9 +176,8 @@ void AtomicWidget::runSwap(QString seller, QString btcChange, QString xmrReceive } -void AtomicWidget::list(QString rendezvous) { +void AtomicWidget::list(const QString& rendezvous) { QStringList arguments; - QList> list; arguments << "--data-base-dir"; arguments << Config::defaultConfigDir().absolutePath(); arguments << "-j"; @@ -204,7 +202,7 @@ void AtomicWidget::list(QString rendezvous) { qDebug() << "parsing Output"; - for(auto line : lines){ + for(const auto& line : lines){ qDebug() << line; if(line.contains("status")){ qDebug() << "status contained"; @@ -212,7 +210,7 @@ void AtomicWidget::list(QString rendezvous) { if (parsedLine["fields"]["status"].toString().contains("Online")){ bool skip = false; auto entry = new OfferEntry(parsedLine["fields"]["price"].toString().split( ' ')[0].toDouble(),parsedLine["fields"]["min_quantity"].toString().split(' ')[0].toDouble(),parsedLine["fields"]["max_quantity"].toString().split(' ')[0].toDouble(), parsedLine["fields"]["address"].toString()); - for(auto post : *offerList){ + for(const auto& post : *offerList){ if(std::equal(entry->address.begin(), entry->address.end(),post->address.begin(),post->address.end())) skip = true; } @@ -237,7 +235,7 @@ void AtomicWidget::list(QString rendezvous) { } AtomicWidget::~AtomicWidget() { - qDebug()<< "EXiting widget!!"; + qDebug()<< "Exiting widget!!"; delete o_model; delete offerList; clean(); @@ -246,7 +244,7 @@ AtomicWidget::~AtomicWidget() { } void AtomicWidget::clean() { - for (auto proc : *procList){ + for (const auto& proc : *procList){ if(!proc->atEnd()) proc->terminate(); } diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h index 1b4bfb19..253f6957 100644 --- a/src/plugins/atomic/AtomicWidget.h +++ b/src/plugins/atomic/AtomicWidget.h @@ -25,7 +25,7 @@ Q_OBJECT public: explicit AtomicWidget(QWidget *parent = nullptr); ~AtomicWidget() override; - void list(QString rendezvous); + void list(const QString& rendezvous); public slots: void skinChanged(); @@ -33,7 +33,7 @@ public slots: private slots: void showAtomicConfigureDialog(); - void runSwap(QString seller, QString btcChange, QString xmrReceive); + void runSwap(const QString& seller, const QString& btcChange, const QString& xmrReceive); signals: void receivedBTC(float new_amount); private: From 08328d96f45d5bfdf13d443d31cb230a074bbca8 Mon Sep 17 00:00:00 2001 From: twiddle Date: Tue, 23 Jul 2024 16:21:00 -0400 Subject: [PATCH 14/26] DONE: 1. Connect signals to make status of swap reflected in AtomicSwap dialog 2. Add informational tabs to AtomicSwap dialog TODO: 3. Add cancel and refund functionality to AtomicSwap when things go wrong, possibly add automatic cancel functionality 4. Add History and recovery to atomic window --- src/assets.qrc | 1 + src/assets/images/hint-icon.png | Bin 0 -> 25420 bytes src/plugins/atomic/AtomicFundDialog.cpp | 9 +- src/plugins/atomic/AtomicFundDialog.h | 3 +- src/plugins/atomic/AtomicFundDialog.ui | 216 ++++++++++++------------ src/plugins/atomic/AtomicSwap.cpp | 5 + src/plugins/atomic/AtomicSwap.ui | 10 ++ src/plugins/atomic/AtomicWidget.cpp | 85 +++++----- src/plugins/atomic/AtomicWidget.h | 5 +- 9 files changed, 180 insertions(+), 154 deletions(-) create mode 100644 src/assets/images/hint-icon.png diff --git a/src/assets.qrc b/src/assets.qrc index b0ab1879..474f4c2c 100644 --- a/src/assets.qrc +++ b/src/assets.qrc @@ -41,6 +41,7 @@ assets/images/file_manager_32px.png assets/images/gnome-calc.png assets/images/atomic-icon.png + assets/images/hint-icon.png assets/images/hd_32px.png assets/images/history.png assets/images/i2p.png diff --git a/src/assets/images/hint-icon.png b/src/assets/images/hint-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4734fd471d5c3772f2200a9d0c4ad8d6c308e262 GIT binary patch literal 25420 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 gAOPXj #include -AtomicFundDialog::AtomicFundDialog(QWidget *parent, QrCode *qrCode, const QString &title, const QString &btc_address) +AtomicFundDialog::AtomicFundDialog(QWidget *parent, const QString &title, const QString &btc_address) : WindowModalDialog(parent) , ui(new Ui::AtomicFundDialog) , address(btc_address) + , qrCode(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH) { ui->setupUi(this); this->setWindowTitle(title); - ui->qrWidget->setQrCode(qrCode); - m_pixmap = qrCode->toPixmap(1).scaled(500, 500, Qt::KeepAspectRatio); + ui->qrWidget->setQrCode(&qrCode); + m_pixmap = qrCode.toPixmap(1).scaled(500, 500, Qt::KeepAspectRatio); + connect(ui->btn_CopyAddress, &QPushButton::clicked, this, &AtomicFundDialog::copyAddress); connect(ui->btn_CopyImage, &QPushButton::clicked, this, &AtomicFundDialog::copyImage); connect(ui->btn_Save, &QPushButton::clicked, this, &AtomicFundDialog::saveImage); connect(ui->btn_Close, &QPushButton::clicked, [this](){ - emit cleanProcs(); accept(); }); diff --git a/src/plugins/atomic/AtomicFundDialog.h b/src/plugins/atomic/AtomicFundDialog.h index 467664e6..b3e4bea7 100644 --- a/src/plugins/atomic/AtomicFundDialog.h +++ b/src/plugins/atomic/AtomicFundDialog.h @@ -20,7 +20,7 @@ class AtomicFundDialog : public WindowModalDialog { Q_OBJECT public: - explicit AtomicFundDialog(QWidget *parent, QrCode *qrCode, const QString &title = "Qr Code", const QString &btc_address = "Error Restart swap"); + explicit AtomicFundDialog(QWidget *parent, const QString &title = "Qr Code", const QString &btc_address = "Error Restart swap"); ~AtomicFundDialog() override; signals: void cleanProcs(); @@ -31,6 +31,7 @@ class AtomicFundDialog : public WindowModalDialog { QScopedPointer ui; QPixmap m_pixmap; QString address; + QrCode qrCode; }; diff --git a/src/plugins/atomic/AtomicFundDialog.ui b/src/plugins/atomic/AtomicFundDialog.ui index 29cfc439..601fb77c 100644 --- a/src/plugins/atomic/AtomicFundDialog.ui +++ b/src/plugins/atomic/AtomicFundDialog.ui @@ -1,111 +1,111 @@ - AtomicFundDialog - - - - 0 - 0 - 520 - 446 - - - - Dialog - - - - - - - 0 - 0 - - - - - 150 - 150 - - - - - - - - QLayout::SetDefaultConstraint - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Copy Address - - - false - - - true - - - - - - - Copy Image - - - false - - - - - - - Save - - - false - - - - - - - Cancel - - - false - - - true - - - - - - + AtomicFundDialog + + + + 0 + 0 + 520 + 446 + + + + Dialog + + + + + + + 0 + 0 + + + + + 150 + 150 + + - - - QrCodeWidget - QWidget -
widgets/QrCodeWidget.h
- 1 -
-
- - -
\ No newline at end of file + + + + + QLayout::SizeConstraint::SetDefaultConstraint + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Copy Address + + + false + + + true + + + + + + + Copy Image + + + false + + + + + + + Save + + + false + + + + + + + Close + + + false + + + true + + + + + +
+
+ + + QrCodeWidget + QWidget +
widgets/QrCodeWidget.h
+ 1 +
+
+ + + diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp index 83afe1b0..e02d45c3 100644 --- a/src/plugins/atomic/AtomicSwap.cpp +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -14,6 +14,11 @@ AtomicSwap::AtomicSwap(QWidget *parent) : ui->setupUi(this); //ui->debug_log->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); ui->label_status->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); + QPixmap pixmapTarget = QPixmap(":/assets/images/hint-icon.png"); + int size=20; + pixmapTarget = pixmapTarget.scaled(size-5, size-5, Qt::KeepAspectRatio, Qt::SmoothTransformation); + ui->btc_hint->setPixmap(pixmapTarget); + ui->btc_hint->setToolTip("Alice is expected to send monero lock after one btc confirmation,\nswap is cancelable after 72 btc confirmations,\nyou will lose your funds if you don't refund before 144 confirmations"); this->setContentsMargins(3,3,3,3); this->adjustSize(); } diff --git a/src/plugins/atomic/AtomicSwap.ui b/src/plugins/atomic/AtomicSwap.ui index 9a1ae4ec..0056e09b 100644 --- a/src/plugins/atomic/AtomicSwap.ui +++ b/src/plugins/atomic/AtomicSwap.ui @@ -95,6 +95,16 @@
+ + + + hint-icon.png + + + Qt::TextFormat::PlainText + + + diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 5a69383f..5aa1efbd 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -23,6 +23,7 @@ AtomicWidget::AtomicWidget(QWidget *parent) , offerList(new QList>()) , swapDialog(new AtomicSwap(this)) , procList(new QList>()) + , fundDialog(new AtomicFundDialog(this)) { ui->setupUi(this); @@ -58,16 +59,20 @@ AtomicWidget::AtomicWidget(QWidget *parent) connect(ui->btn_swap, &QPushButton::clicked, this, [this]{ auto rows = ui->offerBookTable->selectionModel()->selectedRows(); - if (rows.size() < 1){ - ui->meta_label->setText("You must select an offer to use for swap, refresh if there aren't any"); - } else { - QModelIndex index = rows.at(0); - QString seller = index.sibling(index.row(), 3).data().toString(); + clean(); + // UNCOMENT after testing + //if (rows.size() < 1){ + // ui->meta_label->setText("You must select an offer to use for swap, refresh if there aren't any"); + //} else { + //QModelIndex index = rows.at(0); + //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 QString btcChange = ui->change_address->text(); QString xmrReceive = ui->xmr_address->text(); + sleep(1); runSwap(seller,btcChange, xmrReceive); - } + //} }); connect(ui->btn_addRendezvous, &QPushButton::clicked, this, [this]{ @@ -84,7 +89,6 @@ AtomicWidget::AtomicWidget(QWidget *parent) }); connect(swapDialog,&AtomicSwap::cleanProcs, this, [this]{clean();}); - this->updateStatus(); } @@ -95,10 +99,7 @@ void AtomicWidget::skinChanged() { void AtomicWidget::showAtomicConfigureDialog() { AtomicConfigDialog dialog{this}; - - if (dialog.exec() == QDialog::Accepted) { - - } + dialog.show(); } void AtomicWidget::showAtomicSwapDialog() { @@ -115,57 +116,62 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons arguments << "--data-base-dir"; arguments << Config::defaultConfigDir().absolutePath(); // Remove after testing - //arguments << "--testnet"; + arguments << "--testnet"; + arguments << "--debug"; arguments << "-j"; arguments << "buy-xmr"; arguments << "--change-address"; - //arguments << "tb1qzndh6u8qgl2ee4k4gl9erg947g67hyx03vvgen"; - arguments << btcChange; + arguments << "tb1qzndh6u8qgl2ee4k4gl9erg947g67hyx03vvgen"; + //arguments << btcChange; arguments << "--receive-address"; - //arguments << "78YnzFTp3UUMgtKuAJCP2STcbxRZPDPveJ5YGgfg5doiPahS9suWF1r3JhKqjM1McYBJvu8nhkXExGfXVkU6n5S6AXrg4KP"; - arguments << xmrReceive; + arguments << "78YnzFTp3UUMgtKuAJCP2STcbxRZPDPveJ5YGgfg5doiPahS9suWF1r3JhKqjM1McYBJvu8nhkXExGfXVkU6n5S6AXrg4KP"; + //arguments << xmrReceive; arguments << "--seller"; - //arguments << "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWQA4fXDYLNXgxPsVZmnR8kh2wwHUQnkH9e1Wjc8KyJ7p8"; + arguments << "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWQA4fXDYLNXgxPsVZmnR8kh2wwHUQnkH9e1Wjc8KyJ7p8"; // Remove after testing - //arguments << "--electrum-rpc"; - //arguments << "tcp://127.0.0.1:50001"; - //arguments << "--bitcoin-target-block"; - //arguments << "8"; - //arguments << "--monero-daemon-address"; - //arguments << "http://127.0.0.1:38083"; + arguments << "--electrum-rpc"; + arguments << "tcp://127.0.0.1:50001"; + arguments << "--bitcoin-target-block"; + arguments << "1"; + arguments << "--monero-daemon-address"; + arguments << "node.monerodevs.org:38089"; // Uncomment after testing - arguments << seller; + //arguments << seller; arguments << "--tor-socks5-port"; arguments << m_instance->get(Config::socks5Port).toString(); auto *swap = new QProcess(); procList->append(QSharedPointer(swap)); - - swap->setReadChannel(QProcess::StandardError); - connect(swap, &QProcess::readyReadStandardError,this, [this, swap] { + swap->setProcessChannelMode(QProcess::MergedChannels); + swap->setReadChannel(QProcess::StandardOutput); + connect(swap, &QProcess::readyRead,this, [this, swap] { while(swap->canReadLine()){ QJsonParseError err; const QByteArray& rawline = swap->readLine(); QJsonDocument line = QJsonDocument::fromJson(rawline, &err); qDebug() << rawline; + bool check; if (line["fields"]["message"].toString().contains("Connected to Alice")){ qDebug() << "Successfully connected"; swapDialog->logLine(line["fields"].toString()); } else if (!line["fields"]["deposit_address"].toString().isEmpty()){ qDebug() << "Deposit to btc to segwit address"; QString address = line["fields"]["deposit_address"].toString(); - QrCode qrc(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH); - AtomicFundDialog dialog(qobject_cast(parent()), &qrc, "Deposit BTC to this address", address); - connect(&dialog,&AtomicFundDialog::cleanProcs, this, [this]{clean();}); - connect(this, &AtomicWidget::receivedBTC,&dialog, [ &dialog]{ - disconnect(&dialog, SIGNAL(cleanProcs()), nullptr, nullptr); - dialog.close();}); - dialog.exec(); + fundDialog = new AtomicFundDialog(this, "Deposit BTC to this address", address); + //dialog->setModal(true); + fundDialog->show(); } else if (line["fields"]["message"].toString().startsWith("Received Bitcoin")){ - emit receivedBTC(line["fields"]["new_balance"].toString().split(" ")[0].toFloat()); + swapDialog->updateStatus(line["fields"]["new_balance"].toString().split(" ")[0] + " BTC received, starting swap"); + fundDialog->close(); qDebug() << "Spawn atomic swap progress dialog"; showAtomicSwapDialog(); + } else if ( QString confs = line["fields"]["seen_confirmations"].toString(); !confs.isEmpty()){ + qDebug() << "Updating xmrconfs " + confs; + swapDialog->updateXMRConf(confs.toInt()); + } else if (QString message = line["fields"]["message"].toString(); !QString::compare(message, "Bitcoin transaction status changed")){ + qDebug() << "Updating btconfs " + line["fields"]["new_status"].toString().split(" ")[2]; + swapDialog->updateBTCConf(line["fields"]["new_status"].toString().split(" ")[2].toInt()); } //Insert line conditionals here } @@ -245,15 +251,16 @@ AtomicWidget::~AtomicWidget() { void AtomicWidget::clean() { for (const auto& proc : *procList){ - if(!proc->atEnd()) - proc->terminate(); + proc->kill(); } if(QString::compare("WINDOWS",m_instance->get(Config::operatingSystem).toString()) != 0) { qDebug() << "Closing monero-wallet-rpc"; - (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + + (new QProcess)->start("kill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + "/mainnet/monero/monero-wallet-rpc"}); - (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + + (new QProcess)->start("kill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + "/testnet/monero/monero-wallet-rpc"}); } } + + diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h index 253f6957..be4de5e5 100644 --- a/src/plugins/atomic/AtomicWidget.h +++ b/src/plugins/atomic/AtomicWidget.h @@ -13,6 +13,7 @@ #include "OfferModel.h" #include "AtomicSwap.h" #include "config.h" +#include "AtomicFundDialog.h" namespace Ui { class AtomicWidget; @@ -34,8 +35,7 @@ public slots: private slots: void showAtomicConfigureDialog(); void runSwap(const QString& seller, const QString& btcChange, const QString& xmrReceive); -signals: - void receivedBTC(float new_amount); + private: void updateStatus(); @@ -45,6 +45,7 @@ private slots: OfferModel *o_model; QList> *offerList; AtomicSwap *swapDialog; + AtomicFundDialog *fundDialog; void showAtomicSwapDialog(); From 06c43a707ea145513846c189e88c039c03d30d58 Mon Sep 17 00:00:00 2001 From: twiddle Date: Fri, 2 Aug 2024 17:09:22 -0400 Subject: [PATCH 15/26] DONE: 1. Connect signals to make status of swap reflected in AtomicSwap dialog 2. Add informational tabs to AtomicSwap dialog 4. Add recovery to atomic widget TODO: 3. Add cancel and refund functionality to AtomicSwap when things go wrong 4. Refactor AtomicWidget so AtomicSwap handles parsing of swap binary output. --- src/plugins/atomic/AtomicRecoverDialog.cpp | 59 ++++++++++++++++++++++ src/plugins/atomic/AtomicRecoverDialog.h | 30 +++++++++++ src/plugins/atomic/AtomicRecoverDialog.ui | 29 +++++++++++ src/plugins/atomic/AtomicSwap.cpp | 13 ++++- src/plugins/atomic/AtomicSwap.h | 7 ++- src/plugins/atomic/AtomicSwap.ui | 2 +- src/plugins/atomic/AtomicWidget.cpp | 19 +++++-- src/plugins/atomic/AtomicWidget.h | 3 +- src/plugins/atomic/AtomicWidget.ui | 3 ++ src/plugins/atomic/History.h | 17 +++++++ src/utils/config.cpp | 1 + src/utils/config.h | 2 + 12 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 src/plugins/atomic/AtomicRecoverDialog.cpp create mode 100644 src/plugins/atomic/AtomicRecoverDialog.h create mode 100644 src/plugins/atomic/AtomicRecoverDialog.ui create mode 100644 src/plugins/atomic/History.h diff --git a/src/plugins/atomic/AtomicRecoverDialog.cpp b/src/plugins/atomic/AtomicRecoverDialog.cpp new file mode 100644 index 00000000..118e00a6 --- /dev/null +++ b/src/plugins/atomic/AtomicRecoverDialog.cpp @@ -0,0 +1,59 @@ +// +// Created by dev on 7/29/24. +// + +// You may need to build the project (run Qt uic code generator) to get "ui_AtomicRecoverDialog.h" resolved + +#include "AtomicRecoverDialog.h" +#include "ui_AtomicRecoverDialog.h" +#include "History.h" +#include "config.h" +#include + +AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : + WindowModalDialog(parent), ui(new Ui::AtomicRecoverDialog) { + ui->setupUi(this); + auto model = new QStandardItemModel(); + ui->swap_history->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + ui->swap_history->setModel(model); + model->setHorizontalHeaderItem(0, new QStandardItem("Swap-Id")); + model->setHorizontalHeaderItem(1, new QStandardItem("Timestamp swap started")); + model->setHorizontalHeaderItem(2, new QStandardItem("Status")); + + QList rowData; + auto data = Config::instance()->get(Config::pendingSwap).value(); + for(int i=0; i< data.size(); i++){ + auto entry = data[i].value(); + qint64 difference = entry.timestamp.secsTo(QDateTime::currentDateTime()); + + if (difference < 86400) { + rowData.clear(); + rowData << new QStandardItem(entry.id); + rowData << new QStandardItem(entry.timestamp.toString("MM-dd-yyyy hh:mm")); + if (difference > 43200){ + rowData << new QStandardItem("Refundable"); + } else + rowData << new QStandardItem("Recoverable/Pending Refund Timelock"); + model->appendRow(rowData); + } else { + data.remove(i); + } + } + Config::instance()->set(Config::pendingSwap,data); +} + +bool AtomicRecoverDialog::historyEmpty(){ + return Config::instance()->get(Config::pendingSwap).value().isEmpty(); +} + + +void AtomicRecoverDialog::appendHistory(HistoryEntry entry){ + auto current = Config::instance()->get(Config::pendingSwap).value(); + auto var = QVariant(); + var.setValue(entry); + current.append(var); + Config::instance()->set(Config::pendingSwap, current); +} +AtomicRecoverDialog::~AtomicRecoverDialog() { + delete ui; +} diff --git a/src/plugins/atomic/AtomicRecoverDialog.h b/src/plugins/atomic/AtomicRecoverDialog.h new file mode 100644 index 00000000..8e5099e9 --- /dev/null +++ b/src/plugins/atomic/AtomicRecoverDialog.h @@ -0,0 +1,30 @@ +// +// Created by dev on 7/29/24. +// + +#ifndef FEATHER_ATOMICRECOVERDIALOG_H +#define FEATHER_ATOMICRECOVERDIALOG_H + +#include +#include "components.h" +#include "History.h" + +QT_BEGIN_NAMESPACE +namespace Ui { class AtomicRecoverDialog; } +QT_END_NAMESPACE + +class AtomicRecoverDialog : public WindowModalDialog { +Q_OBJECT + +public: + explicit AtomicRecoverDialog(QWidget *parent = nullptr); + bool historyEmpty(); + void appendHistory(HistoryEntry entry); + ~AtomicRecoverDialog() override; + +private: + Ui::AtomicRecoverDialog *ui; +}; + + +#endif //FEATHER_ATOMICRECOVERDIALOG_H diff --git a/src/plugins/atomic/AtomicRecoverDialog.ui b/src/plugins/atomic/AtomicRecoverDialog.ui new file mode 100644 index 00000000..30d56ff2 --- /dev/null +++ b/src/plugins/atomic/AtomicRecoverDialog.ui @@ -0,0 +1,29 @@ + + + AtomicRecoverDialog + + + + 0 + 0 + 903 + 248 + + + + AtomicRecoverDialog + + + + + 10 + 20 + 881 + 179 + + + + + + + diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp index e02d45c3..0e72db81 100644 --- a/src/plugins/atomic/AtomicSwap.cpp +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -5,12 +5,14 @@ // You may need to build the project (run Qt uic code generator) to get "ui_AtomicSwap.h" resolved #include "AtomicSwap.h" + +#include #include "ui_AtomicSwap.h" #include "AtomicWidget.h" AtomicSwap::AtomicSwap(QWidget *parent) : - WindowModalDialog(parent), ui(new Ui::AtomicSwap) { + WindowModalDialog(parent), ui(new Ui::AtomicSwap), fundDialog( new AtomicFundDialog(this)) { ui->setupUi(this); //ui->debug_log->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); ui->label_status->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); @@ -21,6 +23,7 @@ AtomicSwap::AtomicSwap(QWidget *parent) : ui->btc_hint->setToolTip("Alice is expected to send monero lock after one btc confirmation,\nswap is cancelable after 72 btc confirmations,\nyou will lose your funds if you don't refund before 144 confirmations"); this->setContentsMargins(3,3,3,3); this->adjustSize(); + connect(ui->btn_cancel, &QPushButton::clicked, this, &AtomicSwap::cancel); } @@ -44,6 +47,7 @@ void AtomicSwap::updateXMRConf(int confs) { } void AtomicSwap::updateBTCConf(int confs) { + btc_confs = confs; ui->label_btc_cons->setText(QString::number(confs)); this->update(); } @@ -53,3 +57,10 @@ void AtomicSwap::setTitle(QString title) { this->update(); } +void AtomicSwap::setSwap(QString swapId){ + id = std::move(swapId); +} + +void AtomicSwap::cancel(){ + +} diff --git a/src/plugins/atomic/AtomicSwap.h b/src/plugins/atomic/AtomicSwap.h index b32ee1c4..d013f22e 100644 --- a/src/plugins/atomic/AtomicSwap.h +++ b/src/plugins/atomic/AtomicSwap.h @@ -8,6 +8,7 @@ #include #include #include "components.h" +#include "AtomicFundDialog.h" QT_BEGIN_NAMESPACE @@ -25,11 +26,15 @@ Q_OBJECT void updateBTCConf(int confs); void updateXMRConf(int confs); void setTitle(QString title); + void setSwap(QString swapId); signals: void cleanProcs(); private: Ui::AtomicSwap *ui; - + QString id; + AtomicFundDialog fundDialog; + int btc_confs; + void cancel(); }; diff --git a/src/plugins/atomic/AtomicSwap.ui b/src/plugins/atomic/AtomicSwap.ui index 0056e09b..1fe2b5d8 100644 --- a/src/plugins/atomic/AtomicSwap.ui +++ b/src/plugins/atomic/AtomicSwap.ui @@ -37,7 +37,7 @@ - + Cancel diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 5aa1efbd..a851065f 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -24,6 +24,7 @@ AtomicWidget::AtomicWidget(QWidget *parent) , swapDialog(new AtomicSwap(this)) , procList(new QList>()) , fundDialog(new AtomicFundDialog(this)) + , recoverDialog(new AtomicRecoverDialog(this)) { ui->setupUi(this); @@ -42,7 +43,7 @@ AtomicWidget::AtomicWidget(QWidget *parent) ui->offerBookTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); ui->btn_configure->setEnabled(true); - if (!Config::instance()->get(Config::swapPath).toString().isEmpty()) + if (!m_instance->get(Config::swapPath).toString().isEmpty()) ui->meta_label->setText("Refresh offer book before swapping to prevent errors"); connect(ui->btn_configure, &QPushButton::clicked, this, &AtomicWidget::showAtomicConfigureDialog); @@ -82,13 +83,21 @@ AtomicWidget::AtomicWidget(QWidget *parent) tr("p2p multi address of rendezvous point"), QLineEdit::Normal, "", &ok); if (ok && !text.isEmpty()) { - QStringList copy = Config::instance()->get(Config::rendezVous).toStringList(); + QStringList copy = m_instance->get(Config::rendezVous).toStringList(); copy.append(text); - Config::instance()->set(Config::rendezVous,copy); + m_instance->set(Config::rendezVous,copy); } }); connect(swapDialog,&AtomicSwap::cleanProcs, this, [this]{clean();}); + //Remove after testing + //QVariant var; + //var.setValue(HistoryEntry {QDateTime::currentDateTime(),"test-id"}); + //m_instance->set(Config::pendingSwap, QVariantList{var}); + //auto recd = new AtomicRecoverDialog(); + //if (!recd->historyEmpty()){ + // recd->show(); + //} this->updateStatus(); } @@ -99,7 +108,7 @@ void AtomicWidget::skinChanged() { void AtomicWidget::showAtomicConfigureDialog() { AtomicConfigDialog dialog{this}; - dialog.show(); + dialog.exec(); } void AtomicWidget::showAtomicSwapDialog() { @@ -146,6 +155,7 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons swap->setProcessChannelMode(QProcess::MergedChannels); swap->setReadChannel(QProcess::StandardOutput); 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()){ QJsonParseError err; const QByteArray& rawline = swap->readLine(); @@ -163,6 +173,7 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons fundDialog->show(); } else if (line["fields"]["message"].toString().startsWith("Received Bitcoin")){ swapDialog->updateStatus(line["fields"]["new_balance"].toString().split(" ")[0] + " BTC received, starting swap"); + swapDialog->setSwap(line["span"]["swap_id"].toString()); fundDialog->close(); qDebug() << "Spawn atomic swap progress dialog"; showAtomicSwapDialog(); diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h index be4de5e5..015ea948 100644 --- a/src/plugins/atomic/AtomicWidget.h +++ b/src/plugins/atomic/AtomicWidget.h @@ -14,6 +14,7 @@ #include "AtomicSwap.h" #include "config.h" #include "AtomicFundDialog.h" +#include "AtomicRecoverDialog.h" namespace Ui { class AtomicWidget; @@ -46,7 +47,7 @@ private slots: QList> *offerList; AtomicSwap *swapDialog; AtomicFundDialog *fundDialog; - + AtomicRecoverDialog *recoverDialog; void showAtomicSwapDialog(); QList> *procList; diff --git a/src/plugins/atomic/AtomicWidget.ui b/src/plugins/atomic/AtomicWidget.ui index 90ef700a..212fbbd4 100644 --- a/src/plugins/atomic/AtomicWidget.ui +++ b/src/plugins/atomic/AtomicWidget.ui @@ -64,6 +64,9 @@ Qt::Orientation::Horizontal + + QSizePolicy::Policy::Expanding + 40 diff --git a/src/plugins/atomic/History.h b/src/plugins/atomic/History.h new file mode 100644 index 00000000..99904b72 --- /dev/null +++ b/src/plugins/atomic/History.h @@ -0,0 +1,17 @@ +// +// Created by dev on 7/29/24. +// + +#ifndef FEATHER_HISTORY_H +#define FEATHER_HISTORY_H + +#include +#include + +struct HistoryEntry { + QDateTime timestamp; + QString id; +}; + +Q_DECLARE_METATYPE(HistoryEntry); +#endif //FEATHER_HISTORY_H diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 53582696..47e7b992 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -149,6 +149,7 @@ static const QHash configStrings = { "/dns4/swap.sethforprivacy.com/tcp/8888/p2p/12D3KooWCULyZKuV9YEkb6BX8FuwajdvktSzmMg4U5ZX2uYZjHeu"}}}, {Config::swapPath, {QS("swapPath"), ""}}, {Config::operatingSystem, {QS("operatingSystem"), OS}}, + {Config::pendingSwap, {QS("pendingSwap"), QVariantList{}}}, }; diff --git a/src/utils/config.h b/src/utils/config.h index dcf6f30f..0aab6392 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -12,6 +12,7 @@ #include + class Config : public QObject { Q_OBJECT @@ -154,6 +155,7 @@ class Config : public QObject rendezVous, swapPath, operatingSystem, + pendingSwap, }; enum PrivacyLevel { From c67fedf9bc0e7d31b1a8004123301cc6e9a11fd5 Mon Sep 17 00:00:00 2001 From: twiddle Date: Sun, 4 Aug 2024 22:49:31 -0400 Subject: [PATCH 16/26] DONE: 1. Connect signals to make status of swap reflected in AtomicSwap dialog 2. Add informational tabs to AtomicSwap dialog 4. Add recovery to atomic widget 5. Refactor AtomicWidget so AtomicSwap handles parsing of swap binary output. TODO: 3. Add cancel and refund functionality to AtomicSwap when things go wrong --- src/plugins/atomic/AtomicSwap.cpp | 56 +++++++++++++++++++++++++++-- src/plugins/atomic/AtomicSwap.h | 6 +++- src/plugins/atomic/AtomicWidget.cpp | 48 +++---------------------- 3 files changed, 63 insertions(+), 47 deletions(-) diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp index 0e72db81..73092ddf 100644 --- a/src/plugins/atomic/AtomicSwap.cpp +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -7,12 +7,13 @@ #include "AtomicSwap.h" #include +#include #include "ui_AtomicSwap.h" #include "AtomicWidget.h" AtomicSwap::AtomicSwap(QWidget *parent) : - WindowModalDialog(parent), ui(new Ui::AtomicSwap), fundDialog( new AtomicFundDialog(this)) { + WindowModalDialog(parent), ui(new Ui::AtomicSwap), fundDialog( new AtomicFundDialog(this)), procList(new QList>()) { ui->setupUi(this); //ui->debug_log->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); ui->label_status->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); @@ -26,10 +27,61 @@ AtomicSwap::AtomicSwap(QWidget *parent) : connect(ui->btn_cancel, &QPushButton::clicked, this, &AtomicSwap::cancel); } +void AtomicSwap::runSwap(QStringList arguments){ + auto *swap = new QProcess(); + procList->append(QSharedPointer(swap)); + swap->setProcessChannelMode(QProcess::MergedChannels); + swap->setReadChannel(QProcess::StandardOutput); + 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()){ + QJsonParseError err; + const QByteArray& rawline = swap->readLine(); + QJsonDocument line = QJsonDocument::fromJson(rawline, &err); + qDebug() << rawline; + bool check; + if (line["fields"]["message"].toString().contains("Connected to Alice")){ + qDebug() << "Successfully connected"; + this->logLine(line["fields"].toString()); + } else if (!line["fields"]["deposit_address"].toString().isEmpty()){ + qDebug() << "Deposit to btc to segwit address"; + QString address = line["fields"]["deposit_address"].toString(); + fundDialog = new AtomicFundDialog(this, "Deposit BTC to this address", address); + //dialog->setModal(true); + fundDialog->show(); + } else if (line["fields"]["message"].toString().startsWith("Received Bitcoin")){ + this->updateStatus(line["fields"]["new_balance"].toString().split(" ")[0] + " BTC received, starting swap"); + this->setSwap(line["span"]["swap_id"].toString()); + fundDialog->close(); + qDebug() << "Spawn atomic swap progress dialog"; + this->show(); + } else if ( QString confs = line["fields"]["seen_confirmations"].toString(); !confs.isEmpty()){ + qDebug() << "Updating xmrconfs " + confs; + this->updateXMRConf(confs.toInt()); + } else if (QString message = line["fields"]["message"].toString(); !QString::compare(message, "Bitcoin transaction status changed")){ + qDebug() << "Updating btconfs " + line["fields"]["new_status"].toString().split(" ")[2]; + this->updateBTCConf(line["fields"]["new_status"].toString().split(" ")[2].toInt()); + } + //Insert line conditionals here + } + }); + swap->start(Config::instance()->get(Config::swapPath).toString(),arguments); + qDebug() << "process started"; +} AtomicSwap::~AtomicSwap() { delete ui; - emit cleanProcs(); + for (const auto& proc : *procList){ + proc->kill(); + } + if(QString::compare("WINDOWS",Config::instance()->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){ //ui->debug_log->setText(ui->debug_log->toPlainText().append(QTime::currentTime().toString() + ":" + line)); diff --git a/src/plugins/atomic/AtomicSwap.h b/src/plugins/atomic/AtomicSwap.h index d013f22e..3cb7d563 100644 --- a/src/plugins/atomic/AtomicSwap.h +++ b/src/plugins/atomic/AtomicSwap.h @@ -7,6 +7,7 @@ #include #include +#include #include "components.h" #include "AtomicFundDialog.h" @@ -27,12 +28,15 @@ Q_OBJECT void updateXMRConf(int confs); void setTitle(QString title); void setSwap(QString swapId); +public slots: + void runSwap(QStringList swap); signals: void cleanProcs(); private: Ui::AtomicSwap *ui; QString id; - AtomicFundDialog fundDialog; + AtomicFundDialog* fundDialog; + QList>* procList; int btc_confs; void cancel(); diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index a851065f..5c32e065 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -88,7 +88,7 @@ AtomicWidget::AtomicWidget(QWidget *parent) m_instance->set(Config::rendezVous,copy); } }); - connect(swapDialog,&AtomicSwap::cleanProcs, this, [this]{clean();}); + //Remove after testing //QVariant var; @@ -136,7 +136,7 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons arguments << "78YnzFTp3UUMgtKuAJCP2STcbxRZPDPveJ5YGgfg5doiPahS9suWF1r3JhKqjM1McYBJvu8nhkXExGfXVkU6n5S6AXrg4KP"; //arguments << xmrReceive; arguments << "--seller"; - arguments << "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWQA4fXDYLNXgxPsVZmnR8kh2wwHUQnkH9e1Wjc8KyJ7p8"; + arguments << "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooW9yDFYojXnZRdqS9UXcfP2amgwoYdSjujwWdRw4LTSdWw"; // Remove after testing arguments << "--electrum-rpc"; arguments << "tcp://127.0.0.1:50001"; @@ -148,48 +148,7 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons //arguments << seller; arguments << "--tor-socks5-port"; arguments << m_instance->get(Config::socks5Port).toString(); - - - auto *swap = new QProcess(); - procList->append(QSharedPointer(swap)); - swap->setProcessChannelMode(QProcess::MergedChannels); - swap->setReadChannel(QProcess::StandardOutput); - 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()){ - QJsonParseError err; - const QByteArray& rawline = swap->readLine(); - QJsonDocument line = QJsonDocument::fromJson(rawline, &err); - qDebug() << rawline; - bool check; - if (line["fields"]["message"].toString().contains("Connected to Alice")){ - qDebug() << "Successfully connected"; - swapDialog->logLine(line["fields"].toString()); - } else if (!line["fields"]["deposit_address"].toString().isEmpty()){ - qDebug() << "Deposit to btc to segwit address"; - QString address = line["fields"]["deposit_address"].toString(); - fundDialog = new AtomicFundDialog(this, "Deposit BTC to this address", address); - //dialog->setModal(true); - fundDialog->show(); - } else if (line["fields"]["message"].toString().startsWith("Received Bitcoin")){ - swapDialog->updateStatus(line["fields"]["new_balance"].toString().split(" ")[0] + " BTC received, starting swap"); - swapDialog->setSwap(line["span"]["swap_id"].toString()); - fundDialog->close(); - qDebug() << "Spawn atomic swap progress dialog"; - showAtomicSwapDialog(); - } else if ( QString confs = line["fields"]["seen_confirmations"].toString(); !confs.isEmpty()){ - qDebug() << "Updating xmrconfs " + confs; - swapDialog->updateXMRConf(confs.toInt()); - } else if (QString message = line["fields"]["message"].toString(); !QString::compare(message, "Bitcoin transaction status changed")){ - qDebug() << "Updating btconfs " + line["fields"]["new_status"].toString().split(" ")[2]; - swapDialog->updateBTCConf(line["fields"]["new_status"].toString().split(" ")[2].toInt()); - } - //Insert line conditionals here - } - }); - - swap->start(m_instance->get(Config::swapPath).toString(),arguments); - qDebug() << "process started"; + swapDialog->runSwap(arguments); } @@ -253,6 +212,7 @@ void AtomicWidget::list(const QString& rendezvous) { AtomicWidget::~AtomicWidget() { qDebug()<< "Exiting widget!!"; + delete swapDialog; delete o_model; delete offerList; clean(); From 41f78b5f892b061a33e6686d417ddb03411ae377 Mon Sep 17 00:00:00 2001 From: twiddle Date: Thu, 8 Aug 2024 23:49:37 -0400 Subject: [PATCH 17/26] Minor Refactoring and addition of behind the scene changes to swap settings based on feather configuration. Atomic will no longer appear if feather is launched in testnet mode. DONE: 1. Connect signals to make status of swap reflected in AtomicSwap dialog 2. Add informational tabs to AtomicSwap dialog 4. Add recovery to atomic widget 5. Refactor AtomicWidget so AtomicSwap handles parsing of swap binary output. TODO: 3. Add cancel and refund functionality to AtomicSwap when things go wrong --- src/MainWindow.cpp | 1 + src/plugins/PluginRegistry.h | 3 +- src/plugins/atomic/AtomicConfigDialog.cpp | 6 +- src/plugins/atomic/AtomicPlugin.cpp | 11 +++- src/plugins/atomic/AtomicRecoverDialog.cpp | 46 ++++++++++++-- src/plugins/atomic/AtomicRecoverDialog.h | 5 ++ src/plugins/atomic/AtomicRecoverDialog.ui | 61 +++++++++++++++++-- src/plugins/atomic/AtomicSwap.cpp | 14 ++++- src/plugins/atomic/AtomicWidget.cpp | 70 ++++++++++++---------- src/plugins/atomic/AtomicWidget.h | 4 -- 10 files changed, 165 insertions(+), 56 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 9c8814dd..62c880b4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -69,6 +69,7 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa this->restoreGeo(); this->initStatusBar(); + this->initPlugins(); this->initWidgets(); this->initMenu(); diff --git a/src/plugins/PluginRegistry.h b/src/plugins/PluginRegistry.h index 020d2b29..e63aef4b 100644 --- a/src/plugins/PluginRegistry.h +++ b/src/plugins/PluginRegistry.h @@ -6,6 +6,7 @@ #include "Plugin.h" #include "utils/config.h" +#include "constants.h" class PluginRegistry { public: @@ -35,7 +36,7 @@ class PluginRegistry { } bool isPluginEnabled(const QString &id) { - if (!pluginMap.contains(id)) { + if (!pluginMap.contains(id) or (QString::compare(id,"atomic")==0 && constants::networkType==NetworkType::TESTNET)) { return false; } diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp index c4fc4124..c9dbfe5d 100644 --- a/src/plugins/atomic/AtomicConfigDialog.cpp +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -90,7 +90,7 @@ void AtomicConfigDialog::extract() { swapPath.append("/swapTool"); QFile binaryFile(swapPath); binaryFile.open(QIODevice::WriteOnly); - auto operatingSystem = Config::instance()->get(Config::operatingSystem).toString().toStdString(); + auto operatingSystem = conf()->get(Config::operatingSystem).toString().toStdString(); if(strcmp("WIN",operatingSystem.c_str()) == 0) { // UNZIP zip *z = zip_open(tempFile.toStdString().c_str(), 0, 0); @@ -114,7 +114,7 @@ void AtomicConfigDialog::extract() { zip_fclose(f); //And close the archive zip_close(z); - Config::instance()->set(Config::swapPath,swapPath); + conf()->set(Config::swapPath,swapPath); } else { struct archive *a; @@ -145,7 +145,7 @@ void AtomicConfigDialog::extract() { archive_write_close(ext); archive_write_free(ext); - Config::instance()->set(Config::swapPath, QString(savePath.c_str())); + conf()->set(Config::swapPath, QString(savePath.c_str())); } qDebug() << "Finished"; binaryFile.close(); diff --git a/src/plugins/atomic/AtomicPlugin.cpp b/src/plugins/atomic/AtomicPlugin.cpp index ddaf2400..45fea3cc 100644 --- a/src/plugins/atomic/AtomicPlugin.cpp +++ b/src/plugins/atomic/AtomicPlugin.cpp @@ -5,6 +5,7 @@ #include "AtomicConfigDialog.h" #include "plugins/PluginRegistry.h" +#include "constants.h" AtomicPlugin::AtomicPlugin() { @@ -64,7 +65,11 @@ void AtomicPlugin::skinChanged() { } const bool AtomicPlugin::registered = [] { - PluginRegistry::registerPlugin(AtomicPlugin::create()); - PluginRegistry::getInstance().registerPluginCreator(&AtomicPlugin::create); - return true; + if(constants::networkType!=NetworkType::TESTNET) { + PluginRegistry::registerPlugin(AtomicPlugin::create()); + PluginRegistry::getInstance().registerPluginCreator(&AtomicPlugin::create); + return true; + } + + return false; }(); diff --git a/src/plugins/atomic/AtomicRecoverDialog.cpp b/src/plugins/atomic/AtomicRecoverDialog.cpp index 118e00a6..fa00416c 100644 --- a/src/plugins/atomic/AtomicRecoverDialog.cpp +++ b/src/plugins/atomic/AtomicRecoverDialog.cpp @@ -8,20 +8,27 @@ #include "ui_AtomicRecoverDialog.h" #include "History.h" #include "config.h" +#include "AtomicSwap.h" #include AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : - WindowModalDialog(parent), ui(new Ui::AtomicRecoverDialog) { + WindowModalDialog(parent), ui(new Ui::AtomicRecoverDialog), swapDialog(new AtomicSwap(this)) { ui->setupUi(this); auto model = new QStandardItemModel(); ui->swap_history->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); ui->swap_history->setModel(model); + ui->btn_refund_resume->setVisible(false); + ui->swap_history->setSelectionBehavior(QAbstractItemView::SelectRows); + ui->swap_history->verticalHeader()->setVisible(false); + // Makes it easy to see if button is in refund or resume mode + ui->btn_refund_resume->setProperty("Refund",0); + this->setWindowFlag(Qt::WindowStaysOnTopHint); model->setHorizontalHeaderItem(0, new QStandardItem("Swap-Id")); model->setHorizontalHeaderItem(1, new QStandardItem("Timestamp swap started")); model->setHorizontalHeaderItem(2, new QStandardItem("Status")); QList rowData; - auto data = Config::instance()->get(Config::pendingSwap).value(); + auto data = conf()->get(Config::pendingSwap).value(); for(int i=0; i< data.size(); i++){ auto entry = data[i].value(); qint64 difference = entry.timestamp.secsTo(QDateTime::currentDateTime()); @@ -39,20 +46,47 @@ AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : data.remove(i); } } - Config::instance()->set(Config::pendingSwap,data); + conf()->set(Config::pendingSwap,data); + connect(ui->swap_history, &QAbstractItemView::clicked, this, &AtomicRecoverDialog::updateBtn); + connect(ui->btn_refund_resume, &QPushButton::clicked, this, [this]{ + QStringList arguments; + if (ui->btn_refund_resume->property("Refund").toBool()){ + arguments << "cancel-and-refund"; + } else { + arguments << "resume"; + } + arguments << "--swap-id"; + arguments << ui->swap_history->selectionModel()->selectedRows().at(0).sibling(0,1).data().toString(); + arguments << "--tor-socks5-port"; + arguments << conf()->get(Config::socks5Port).toString(); + swapDialog->runSwap(arguments); + }); +} + +void AtomicRecoverDialog::updateBtn(const QModelIndex &index){ + auto row = index.sibling(index.row(),2).data().toString(); + if (row.startsWith("Ref")){ + ui->btn_refund_resume->setText("Cancel And Refund"); + ui->btn_refund_resume->setProperty("Refund",1); + } else { + ui->btn_refund_resume->setText("Attempt to Resume"); + ui->btn_refund_resume->setProperty("Refund",0); + } + ui->btn_refund_resume->setVisible(true); + } bool AtomicRecoverDialog::historyEmpty(){ - return Config::instance()->get(Config::pendingSwap).value().isEmpty(); + return conf()->get(Config::pendingSwap).value().isEmpty(); } void AtomicRecoverDialog::appendHistory(HistoryEntry entry){ - auto current = Config::instance()->get(Config::pendingSwap).value(); + auto current = conf()->get(Config::pendingSwap).value(); auto var = QVariant(); var.setValue(entry); current.append(var); - Config::instance()->set(Config::pendingSwap, current); + conf()->set(Config::pendingSwap, current); } AtomicRecoverDialog::~AtomicRecoverDialog() { delete ui; diff --git a/src/plugins/atomic/AtomicRecoverDialog.h b/src/plugins/atomic/AtomicRecoverDialog.h index 8e5099e9..0558a2f9 100644 --- a/src/plugins/atomic/AtomicRecoverDialog.h +++ b/src/plugins/atomic/AtomicRecoverDialog.h @@ -8,6 +8,7 @@ #include #include "components.h" #include "History.h" +#include "AtomicSwap.h" QT_BEGIN_NAMESPACE namespace Ui { class AtomicRecoverDialog; } @@ -22,8 +23,12 @@ Q_OBJECT void appendHistory(HistoryEntry entry); ~AtomicRecoverDialog() override; +private slots: + void updateBtn(const QModelIndex &index); private: Ui::AtomicRecoverDialog *ui; + AtomicSwap *swapDialog; + }; diff --git a/src/plugins/atomic/AtomicRecoverDialog.ui b/src/plugins/atomic/AtomicRecoverDialog.ui index 30d56ff2..092a9bb3 100644 --- a/src/plugins/atomic/AtomicRecoverDialog.ui +++ b/src/plugins/atomic/AtomicRecoverDialog.ui @@ -6,22 +6,71 @@ 0 0 - 903 - 248 + 936 + 256 AtomicRecoverDialog - + 10 - 20 - 881 - 179 + 10 + 911 + 211 + + + + + There are one or more Atomic Swaps still pending, take action to prevent losing funds! + + + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Resume + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp index 73092ddf..7fa80d90 100644 --- a/src/plugins/atomic/AtomicSwap.cpp +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -60,13 +60,21 @@ void AtomicSwap::runSwap(QStringList arguments){ this->updateXMRConf(confs.toInt()); } else if (QString message = line["fields"]["message"].toString(); !QString::compare(message, "Bitcoin transaction status changed")){ qDebug() << "Updating btconfs " + line["fields"]["new_status"].toString().split(" ")[2]; - this->updateBTCConf(line["fields"]["new_status"].toString().split(" ")[2].toInt()); + QString status = line["fields"]["new_status"].toString(); + bool ok; + auto confirmations = status.split(" ")[2].toInt(&ok, 10); + if(ok) { + this->updateBTCConf(confirmations); + } else { + this->updateStatus("Found txid " + line["fields"]["txid"].toString() + " in mempool"); + } + } //Insert line conditionals here } }); - swap->start(Config::instance()->get(Config::swapPath).toString(),arguments); + swap->start(conf()->get(Config::swapPath).toString(),arguments); qDebug() << "process started"; } AtomicSwap::~AtomicSwap() { @@ -74,7 +82,7 @@ AtomicSwap::~AtomicSwap() { for (const auto& proc : *procList){ proc->kill(); } - if(QString::compare("WINDOWS",Config::instance()->get(Config::operatingSystem).toString()) != 0) { + 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"}); diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 5c32e065..a31d5fc6 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -11,6 +11,7 @@ #include "AtomicConfigDialog.h" #include "OfferModel.h" #include "utils/AppData.h" +#include "utils/nodes.h" #include "utils/ColorScheme.h" #include "utils/WebsocketNotifier.h" #include "AtomicFundDialog.h" @@ -18,7 +19,6 @@ AtomicWidget::AtomicWidget(QWidget *parent) : QWidget(parent) , ui(new Ui::AtomicWidget) - , m_instance(Config::instance()) , o_model(new OfferModel(this)) , offerList(new QList>()) , swapDialog(new AtomicSwap(this)) @@ -43,7 +43,7 @@ AtomicWidget::AtomicWidget(QWidget *parent) ui->offerBookTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); ui->btn_configure->setEnabled(true); - if (!m_instance->get(Config::swapPath).toString().isEmpty()) + if (!conf()->get(Config::swapPath).toString().isEmpty()) ui->meta_label->setText("Refresh offer book before swapping to prevent errors"); connect(ui->btn_configure, &QPushButton::clicked, this, &AtomicWidget::showAtomicConfigureDialog); @@ -53,7 +53,7 @@ AtomicWidget::AtomicWidget(QWidget *parent) ui->meta_label->setText("Updating offer book this may take a bit, if no offers appear after a while try refreshing again"); - QStringList pointList = m_instance->get(Config::rendezVous).toStringList(); + QStringList pointList = conf()->get(Config::rendezVous).toStringList(); for(const QString& point :pointList) AtomicWidget::list(point); }); @@ -83,22 +83,21 @@ AtomicWidget::AtomicWidget(QWidget *parent) tr("p2p multi address of rendezvous point"), QLineEdit::Normal, "", &ok); if (ok && !text.isEmpty()) { - QStringList copy = m_instance->get(Config::rendezVous).toStringList(); + QStringList copy = conf()->get(Config::rendezVous).toStringList(); copy.append(text); - m_instance->set(Config::rendezVous,copy); + conf()->set(Config::rendezVous,copy); } }); //Remove after testing - //QVariant var; - //var.setValue(HistoryEntry {QDateTime::currentDateTime(),"test-id"}); - //m_instance->set(Config::pendingSwap, QVariantList{var}); - //auto recd = new AtomicRecoverDialog(); - //if (!recd->historyEmpty()){ - // recd->show(); - //} - this->updateStatus(); + QVariant var; + var.setValue(HistoryEntry {QDateTime::currentDateTime(),"test-id"}); + conf()->set(Config::pendingSwap, QVariantList{var}); + auto recd = new AtomicRecoverDialog(this); + if (!recd->historyEmpty()){ + recd->show(); + } } @@ -111,13 +110,6 @@ void AtomicWidget::showAtomicConfigureDialog() { dialog.exec(); } -void AtomicWidget::showAtomicSwapDialog() { - swapDialog->show(); -} - -void AtomicWidget::updateStatus() { - -} void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, const QString& xmrReceive) { qDebug() << "starting swap"; @@ -125,7 +117,9 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons arguments << "--data-base-dir"; arguments << Config::defaultConfigDir().absolutePath(); // Remove after testing - arguments << "--testnet"; + if (constants::networkType==NetworkType::STAGENET){ + arguments << "--testnet"; + } arguments << "--debug"; arguments << "-j"; arguments << "buy-xmr"; @@ -142,12 +136,24 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons arguments << "tcp://127.0.0.1:50001"; arguments << "--bitcoin-target-block"; arguments << "1"; + 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(); + } + } + qDebug() << nodes.value("0").toObject()["ws"].toArray()[0]; arguments << "--monero-daemon-address"; - arguments << "node.monerodevs.org:38089"; + //arguments << "node.monerodevs.org:38089"; + arguments << nodes.value("0").toObject()["ws"].toArray()[0].toString(); // Uncomment after testing //arguments << seller; - arguments << "--tor-socks5-port"; - arguments << m_instance->get(Config::socks5Port).toString(); + if(conf()->get(Config::proxy).toInt() != Config::Proxy::None) { + arguments << "--tor-socks5-port"; + arguments << conf()->get(Config::socks5Port).toString(); + } swapDialog->runSwap(arguments); } @@ -156,16 +162,21 @@ void AtomicWidget::list(const QString& rendezvous) { QStringList arguments; arguments << "--data-base-dir"; arguments << Config::defaultConfigDir().absolutePath(); + if (constants::networkType==NetworkType::STAGENET){ + arguments << "--testnet"; + } arguments << "-j"; arguments << "list-sellers"; - arguments << "--tor-socks5-port"; - arguments << m_instance->get(Config::socks5Port).toString(); + if(conf()->get(Config::proxy).toInt() != Config::Proxy::None) { + arguments << "--tor-socks5-port"; + arguments << conf()->get(Config::socks5Port).toString(); + } arguments << "--rendezvous-point"; arguments << rendezvous; auto *swap = new QProcess(); procList->append(QSharedPointer(swap)); swap->setReadChannel(QProcess::StandardError); - //swap->start(m_instance->get(Config::swapPath).toString(), arguments); + //swap->start(conf()->get(Config::swapPath).toString(), arguments); connect(swap, &QProcess::finished, this, [this, swap]{ QJsonDocument parsedLine; QJsonParseError parseError; @@ -203,7 +214,7 @@ void AtomicWidget::list(const QString& rendezvous) { o_model->updateOffers(*offerList); return list; }); - swap->start(m_instance->get(Config::swapPath).toString(), arguments); + swap->start(conf()->get(Config::swapPath).toString(), arguments); //swap->waitForFinished(120000); @@ -216,7 +227,6 @@ AtomicWidget::~AtomicWidget() { delete o_model; delete offerList; clean(); - delete m_instance; delete procList; } @@ -224,7 +234,7 @@ void AtomicWidget::clean() { for (const auto& proc : *procList){ proc->kill(); } - if(QString::compare("WINDOWS",m_instance->get(Config::operatingSystem).toString()) != 0) { + 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"}); diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h index 015ea948..c465ced7 100644 --- a/src/plugins/atomic/AtomicWidget.h +++ b/src/plugins/atomic/AtomicWidget.h @@ -38,8 +38,6 @@ private slots: void runSwap(const QString& seller, const QString& btcChange, const QString& xmrReceive); private: - void updateStatus(); - QScopedPointer ui; bool m_comboBoxInit = false; QTimer m_statusTimer; @@ -48,10 +46,8 @@ private slots: AtomicSwap *swapDialog; AtomicFundDialog *fundDialog; AtomicRecoverDialog *recoverDialog; - void showAtomicSwapDialog(); QList> *procList; - Config *m_instance; }; #endif // FEATHER_ATOMICWIDGET_H From 12bbc76e26276bb4624990ca687ec58323632a77 Mon Sep 17 00:00:00 2001 From: twiddle Date: Sat, 10 Aug 2024 21:03:23 -0400 Subject: [PATCH 18/26] 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 --- src/plugins/atomic/AtomicConfigDialog.cpp | 9 ++- src/plugins/atomic/AtomicRecoverDialog.cpp | 17 ++++- src/plugins/atomic/AtomicSwap.cpp | 51 +++++++------ src/plugins/atomic/AtomicSwap.h | 1 - src/plugins/atomic/AtomicWidget.cpp | 84 ++++++++++------------ src/plugins/atomic/OfferModel.cpp | 1 - src/utils/config.cpp | 1 + src/utils/config.h | 1 + 8 files changed, 91 insertions(+), 74 deletions(-) diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp index c9dbfe5d..b6270744 100644 --- a/src/plugins/atomic/AtomicConfigDialog.cpp +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -58,13 +58,14 @@ void AtomicConfigDialog::downloadBinary() { tempFile = download->fileName(); QString url; 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) { // 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){ - 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 { - 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); @@ -72,7 +73,6 @@ void AtomicConfigDialog::downloadBinary() { QStringList answer; connect(archive,&QNetworkReply::readyRead, this, [this]{ QByteArray data= archive->readAll(); - qDebug() << "received data of size: " << data.size(); download->write(data.constData(), data.size()); }); connect(archive, &QNetworkReply::finished, @@ -82,7 +82,6 @@ void AtomicConfigDialog::downloadBinary() { void AtomicConfigDialog::extract() { ui->downloadLabel->setText("Download Successful, extracting binary to final destination"); - qDebug() << "extracting"; download->close(); archive->deleteLater(); diff --git a/src/plugins/atomic/AtomicRecoverDialog.cpp b/src/plugins/atomic/AtomicRecoverDialog.cpp index fa00416c..e6cd0867 100644 --- a/src/plugins/atomic/AtomicRecoverDialog.cpp +++ b/src/plugins/atomic/AtomicRecoverDialog.cpp @@ -9,6 +9,7 @@ #include "History.h" #include "config.h" #include "AtomicSwap.h" +#include "Utils.h" #include AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : @@ -57,8 +58,20 @@ AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : } arguments << "--swap-id"; arguments << ui->swap_history->selectionModel()->selectedRows().at(0).sibling(0,1).data().toString(); - arguments << "--tor-socks5-port"; - arguments << conf()->get(Config::socks5Port).toString(); + arguments << "--monero-daemon-address"; + 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); }); } diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp index 7fa80d90..74604dc0 100644 --- a/src/plugins/atomic/AtomicSwap.cpp +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -6,8 +6,8 @@ #include "AtomicSwap.h" -#include #include +#include #include "ui_AtomicSwap.h" #include "AtomicWidget.h" @@ -15,7 +15,6 @@ AtomicSwap::AtomicSwap(QWidget *parent) : WindowModalDialog(parent), ui(new Ui::AtomicSwap), fundDialog( new AtomicFundDialog(this)), procList(new QList>()) { ui->setupUi(this); - //ui->debug_log->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); ui->label_status->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); QPixmap pixmapTarget = QPixmap(":/assets/images/hint-icon.png"); int size=20; @@ -33,7 +32,6 @@ void AtomicSwap::runSwap(QStringList arguments){ swap->setProcessChannelMode(QProcess::MergedChannels); swap->setReadChannel(QProcess::StandardOutput); 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()){ QJsonParseError err; const QByteArray& rawline = swap->readLine(); @@ -51,14 +49,18 @@ void AtomicSwap::runSwap(QStringList arguments){ fundDialog->show(); } else if (line["fields"]["message"].toString().startsWith("Received Bitcoin")){ 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(); qDebug() << "Spawn atomic swap progress dialog"; this->show(); } else if ( QString confs = line["fields"]["seen_confirmations"].toString(); !confs.isEmpty()){ qDebug() << "Updating xmrconfs " + confs; 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]; QString status = line["fields"]["new_status"].toString(); bool ok; @@ -68,9 +70,31 @@ void AtomicSwap::runSwap(QStringList arguments){ } else { 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().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){ 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){ - //ui->debug_log->setText(ui->debug_log->toPlainText().append(QTime::currentTime().toString() + ":" + line)); - this->update(); } void AtomicSwap::updateStatus(QString status){ @@ -117,9 +131,6 @@ void AtomicSwap::setTitle(QString title) { this->update(); } -void AtomicSwap::setSwap(QString swapId){ - id = std::move(swapId); -} void AtomicSwap::cancel(){ diff --git a/src/plugins/atomic/AtomicSwap.h b/src/plugins/atomic/AtomicSwap.h index 3cb7d563..f4e2f100 100644 --- a/src/plugins/atomic/AtomicSwap.h +++ b/src/plugins/atomic/AtomicSwap.h @@ -27,7 +27,6 @@ Q_OBJECT void updateBTCConf(int confs); void updateXMRConf(int confs); void setTitle(QString title); - void setSwap(QString swapId); public slots: void runSwap(QStringList swap); signals: diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index a31d5fc6..416b2961 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -61,19 +61,45 @@ AtomicWidget::AtomicWidget(QWidget *parent) connect(ui->btn_swap, &QPushButton::clicked, this, [this]{ auto rows = ui->offerBookTable->selectionModel()->selectedRows(); clean(); - // UNCOMENT after testing - //if (rows.size() < 1){ - // ui->meta_label->setText("You must select an offer to use for swap, refresh if there aren't any"); - //} else { - //QModelIndex index = rows.at(0); - //QString seller = index.sibling(index.row(), 3).data().toString(); - QString seller = "test"; + if (rows.size() < 1){ + ui->meta_label->setText("You must select an offer to use for swap, refresh if there aren't any"); + } else { + QModelIndex index = rows.at(0); + QString seller = index.sibling(index.row(), 3).data().toString(); //Add proper error checking on ui input after rest of swap is implemented 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(); + 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); runSwap(seller,btcChange, xmrReceive); - //} + } }); 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); if (!recd->historyEmpty()){ recd->show(); @@ -124,18 +145,10 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons arguments << "-j"; arguments << "buy-xmr"; arguments << "--change-address"; - arguments << "tb1qzndh6u8qgl2ee4k4gl9erg947g67hyx03vvgen"; - //arguments << btcChange; + arguments << btcChange; arguments << "--receive-address"; - arguments << "78YnzFTp3UUMgtKuAJCP2STcbxRZPDPveJ5YGgfg5doiPahS9suWF1r3JhKqjM1McYBJvu8nhkXExGfXVkU6n5S6AXrg4KP"; - //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"; + arguments << xmrReceive; + auto nodes = conf()->get(Config::nodes).toJsonObject(); if (nodes.isEmpty()) { auto jsonData = conf()->get(Config::nodes).toByteArray(); @@ -144,12 +157,10 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons nodes = doc.object(); } } - qDebug() << nodes.value("0").toObject()["ws"].toArray()[0]; arguments << "--monero-daemon-address"; - //arguments << "node.monerodevs.org:38089"; 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) { arguments << "--tor-socks5-port"; arguments << conf()->get(Config::socks5Port).toString(); @@ -176,23 +187,16 @@ void AtomicWidget::list(const QString& rendezvous) { auto *swap = new QProcess(); procList->append(QSharedPointer(swap)); swap->setReadChannel(QProcess::StandardError); - //swap->start(conf()->get(Config::swapPath).toString(), arguments); connect(swap, &QProcess::finished, this, [this, swap]{ QJsonDocument parsedLine; QJsonParseError parseError; QList> list; - qDebug() << "Subprocess has finished"; auto output = QString::fromLocal8Bit(swap->readAllStandardError()); - qDebug() << "Crashes before splitting"; auto lines = output.split(QRegularExpression("[\r\n]"),Qt::SkipEmptyParts); - qDebug() << lines.size(); - qDebug() << "parsing Output"; for(const auto& line : lines){ - qDebug() << line; if(line.contains("status")){ - qDebug() << "status contained"; parsedLine = QJsonDocument::fromJson(line.toLocal8Bit(), &parseError ); if (parsedLine["fields"]["status"].toString().contains("Online")){ bool skip = false; @@ -205,17 +209,14 @@ void AtomicWidget::list(const QString& rendezvous) { ui->meta_label->setText("Updated offer book"); offerList->append(QSharedPointer(entry)); } - qDebug() << entry; } } } - qDebug() << "exits fine"; swap->close(); o_model->updateOffers(*offerList); return list; }); swap->start(conf()->get(Config::swapPath).toString(), arguments); - //swap->waitForFinished(120000); @@ -234,13 +235,6 @@ void AtomicWidget::clean() { for (const auto& proc : *procList){ 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"}); - } } diff --git a/src/plugins/atomic/OfferModel.cpp b/src/plugins/atomic/OfferModel.cpp index e4092c82..013a0806 100644 --- a/src/plugins/atomic/OfferModel.cpp +++ b/src/plugins/atomic/OfferModel.cpp @@ -21,7 +21,6 @@ void OfferModel::clear() { void OfferModel::updateOffers(const QList> &posts) { beginResetModel(); - qDebug() << "updating Offers"; m_offers.clear(); for (const auto& post : posts) { m_offers.push_back(post); diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 47e7b992..7eef8411 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -150,6 +150,7 @@ static const QHash configStrings = { {Config::swapPath, {QS("swapPath"), ""}}, {Config::operatingSystem, {QS("operatingSystem"), OS}}, {Config::pendingSwap, {QS("pendingSwap"), QVariantList{}}}, + {Config::swapVersion, {QS("swapVersion"), "0.13.4"}}, }; diff --git a/src/utils/config.h b/src/utils/config.h index 0aab6392..d7c91ee1 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -156,6 +156,7 @@ class Config : public QObject swapPath, operatingSystem, pendingSwap, + swapVersion, }; enum PrivacyLevel { From 441aa249963dab9fc93f180322963b00bbeb3a4b Mon Sep 17 00:00:00 2001 From: twiddle Date: Thu, 22 Aug 2024 18:25:41 -0400 Subject: [PATCH 19/26] Version v1.0 Everything should be working, tested on ubuntu. --- src/plugins/atomic/AtomicFundDialog.cpp | 4 +- src/plugins/atomic/AtomicFundDialog.h | 1 + src/plugins/atomic/AtomicFundDialog.ui | 7 +++ src/plugins/atomic/AtomicRecoverDialog.cpp | 47 ++++++++-------- src/plugins/atomic/AtomicRecoverDialog.h | 3 +- src/plugins/atomic/AtomicSwap.cpp | 62 +++++++++++++++------- src/plugins/atomic/AtomicSwap.h | 1 + src/plugins/atomic/AtomicWidget.cpp | 15 +++++- src/plugins/atomic/AtomicWidget.ui | 4 +- src/plugins/atomic/History.h | 17 ------ src/utils/config.cpp | 2 +- 11 files changed, 96 insertions(+), 67 deletions(-) delete mode 100644 src/plugins/atomic/History.h diff --git a/src/plugins/atomic/AtomicFundDialog.cpp b/src/plugins/atomic/AtomicFundDialog.cpp index 4bd5d1dd..6def4ab2 100644 --- a/src/plugins/atomic/AtomicFundDialog.cpp +++ b/src/plugins/atomic/AtomicFundDialog.cpp @@ -51,7 +51,9 @@ void AtomicFundDialog::copyAddress(){ QMessageBox::information(this, "Information", "BTC deposit address copied to clipboard"); } - +void AtomicFundDialog::updateMin(QString min){ + ui->label_status->setText("Deposit at least " + min + " BTC to cover fee"); +} AtomicFundDialog::~AtomicFundDialog() { emit cleanProcs(); diff --git a/src/plugins/atomic/AtomicFundDialog.h b/src/plugins/atomic/AtomicFundDialog.h index b3e4bea7..f9dee47f 100644 --- a/src/plugins/atomic/AtomicFundDialog.h +++ b/src/plugins/atomic/AtomicFundDialog.h @@ -22,6 +22,7 @@ class AtomicFundDialog : public WindowModalDialog { public: explicit AtomicFundDialog(QWidget *parent, const QString &title = "Qr Code", const QString &btc_address = "Error Restart swap"); ~AtomicFundDialog() override; + void updateMin(QString min); signals: void cleanProcs(); private: diff --git a/src/plugins/atomic/AtomicFundDialog.ui b/src/plugins/atomic/AtomicFundDialog.ui index 601fb77c..433a6934 100644 --- a/src/plugins/atomic/AtomicFundDialog.ui +++ b/src/plugins/atomic/AtomicFundDialog.ui @@ -14,6 +14,13 @@ Dialog + + + + + + + diff --git a/src/plugins/atomic/AtomicRecoverDialog.cpp b/src/plugins/atomic/AtomicRecoverDialog.cpp index e6cd0867..8f894043 100644 --- a/src/plugins/atomic/AtomicRecoverDialog.cpp +++ b/src/plugins/atomic/AtomicRecoverDialog.cpp @@ -6,10 +6,10 @@ #include "AtomicRecoverDialog.h" #include "ui_AtomicRecoverDialog.h" -#include "History.h" #include "config.h" #include "AtomicSwap.h" #include "Utils.h" +#include "constants.h" #include AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : @@ -17,27 +17,33 @@ AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : ui->setupUi(this); auto model = new QStandardItemModel(); ui->swap_history->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); - ui->swap_history->setModel(model); + ui->swap_history->setSelectionMode(QAbstractItemView::SingleSelection); ui->btn_refund_resume->setVisible(false); ui->swap_history->setSelectionBehavior(QAbstractItemView::SelectRows); ui->swap_history->verticalHeader()->setVisible(false); // Makes it easy to see if button is in refund or resume mode ui->btn_refund_resume->setProperty("Refund",0); this->setWindowFlag(Qt::WindowStaysOnTopHint); + this->raise(); model->setHorizontalHeaderItem(0, new QStandardItem("Swap-Id")); model->setHorizontalHeaderItem(1, new QStandardItem("Timestamp swap started")); model->setHorizontalHeaderItem(2, new QStandardItem("Status")); QList rowData; - auto data = conf()->get(Config::pendingSwap).value(); + qDebug() << conf()->get(Config::pendingSwap); + QStringList data = conf()->get(Config::pendingSwap).toStringList(); + qDebug() << data; for(int i=0; i< data.size(); i++){ - auto entry = data[i].value(); - qint64 difference = entry.timestamp.secsTo(QDateTime::currentDateTime()); + QStringList entry = data[i].split(":"); + qDebug() << "Swap-id - " + entry[0]; + QString id = entry[0]; + QDateTime timestamp = QDateTime::fromString(entry[1],"dd.MM.yyyy.hh.mm.ss"); + qint64 difference = timestamp.secsTo(QDateTime::currentDateTime()); if (difference < 86400) { rowData.clear(); - rowData << new QStandardItem(entry.id); - rowData << new QStandardItem(entry.timestamp.toString("MM-dd-yyyy hh:mm")); + rowData << new QStandardItem(id); + rowData << new QStandardItem(timestamp.toString("MM-dd-yyyy hh:mm")); if (difference > 43200){ rowData << new QStandardItem("Refundable"); } else @@ -47,27 +53,26 @@ AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : data.remove(i); } } + ui->swap_history->setModel(model); conf()->set(Config::pendingSwap,data); connect(ui->swap_history, &QAbstractItemView::clicked, this, &AtomicRecoverDialog::updateBtn); connect(ui->btn_refund_resume, &QPushButton::clicked, this, [this]{ QStringList arguments; + if (constants::networkType==NetworkType::STAGENET) { + arguments << "--testnet"; + } + arguments << "-j"; + arguments << "--debug"; + arguments << "-d"; + arguments << Config::defaultConfigDir().absolutePath(); if (ui->btn_refund_resume->property("Refund").toBool()){ arguments << "cancel-and-refund"; } else { arguments << "resume"; } arguments << "--swap-id"; - arguments << ui->swap_history->selectionModel()->selectedRows().at(0).sibling(0,1).data().toString(); - arguments << "--monero-daemon-address"; - 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(); + auto row = ui->swap_history->selectionModel()->selectedRows().at(0); + arguments << row.sibling(row.row(),0).data().toString(); if(conf()->get(Config::proxy).toInt() != Config::Proxy::None) { arguments << "--tor-socks5-port"; arguments << conf()->get(Config::socks5Port).toString(); @@ -94,11 +99,9 @@ bool AtomicRecoverDialog::historyEmpty(){ } -void AtomicRecoverDialog::appendHistory(HistoryEntry entry){ +void AtomicRecoverDialog::appendHistory(QString entry){ auto current = conf()->get(Config::pendingSwap).value(); - auto var = QVariant(); - var.setValue(entry); - current.append(var); + current.append(entry); conf()->set(Config::pendingSwap, current); } AtomicRecoverDialog::~AtomicRecoverDialog() { diff --git a/src/plugins/atomic/AtomicRecoverDialog.h b/src/plugins/atomic/AtomicRecoverDialog.h index 0558a2f9..29c871f3 100644 --- a/src/plugins/atomic/AtomicRecoverDialog.h +++ b/src/plugins/atomic/AtomicRecoverDialog.h @@ -7,7 +7,6 @@ #include #include "components.h" -#include "History.h" #include "AtomicSwap.h" QT_BEGIN_NAMESPACE @@ -20,7 +19,7 @@ Q_OBJECT public: explicit AtomicRecoverDialog(QWidget *parent = nullptr); bool historyEmpty(); - void appendHistory(HistoryEntry entry); + void appendHistory(QString entry); ~AtomicRecoverDialog() override; private slots: diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp index 74604dc0..23ce35a0 100644 --- a/src/plugins/atomic/AtomicSwap.cpp +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -10,6 +10,8 @@ #include #include "ui_AtomicSwap.h" #include "AtomicWidget.h" +#include "constants.h" +#include "networktype.h" AtomicSwap::AtomicSwap(QWidget *parent) : @@ -20,7 +22,7 @@ AtomicSwap::AtomicSwap(QWidget *parent) : int size=20; pixmapTarget = pixmapTarget.scaled(size-5, size-5, Qt::KeepAspectRatio, Qt::SmoothTransformation); ui->btc_hint->setPixmap(pixmapTarget); - ui->btc_hint->setToolTip("Alice is expected to send monero lock after one btc confirmation,\nswap is cancelable after 72 btc confirmations,\nyou will lose your funds if you don't refund before 144 confirmations"); + ui->btc_hint->setToolTip("Alice is expected to send monero lock after one btc confirmation,\nswap is cancelable after 72 btc confirmations,\nyou will lose your funds if you don't refund before 144 confirmations\n\nResumed swaps may not have accurate numbers for confirmations!!!"); this->setContentsMargins(3,3,3,3); this->adjustSize(); connect(ui->btn_cancel, &QPushButton::clicked, this, &AtomicSwap::cancel); @@ -38,21 +40,25 @@ void AtomicSwap::runSwap(QStringList arguments){ QJsonDocument line = QJsonDocument::fromJson(rawline, &err); qDebug() << rawline; bool check; - if (line["fields"]["message"].toString().contains("Connected to Alice")){ + QString message = line["fields"]["message"].toString(); + if (message.contains("Connected to Alice")){ qDebug() << "Successfully connected"; this->logLine(line["fields"].toString()); } else if (!line["fields"]["deposit_address"].toString().isEmpty()){ qDebug() << "Deposit to btc to segwit address"; QString address = line["fields"]["deposit_address"].toString(); fundDialog = new AtomicFundDialog(this, "Deposit BTC to this address", address); + fundDialog->updateMin(min); + fundDialog->update(); //dialog->setModal(true); fundDialog->show(); - } else if (line["fields"]["message"].toString().startsWith("Received Bitcoin")){ + } else if (message.startsWith("Received Bitcoin")){ this->updateStatus(line["fields"]["new_balance"].toString().split(" ")[0] + " BTC received, starting swap"); - QVariant var; - var.setValue(HistoryEntry {QDateTime::currentDateTime(),line["span"]["swap_id"].toString()}); + QString entry = line["span"]["swap_id"].toString() + ":" + QDateTime::currentDateTime().toString("dd.MM.yyyy.hh.mm.ss"); + qDebug() << "Swap logged as "; + qDebug() << entry; QVariantList past = conf()->get(Config::pendingSwap).toList(); - past.append(var); + past.append(entry); conf()->set(Config::pendingSwap,past); fundDialog->close(); qDebug() << "Spawn atomic swap progress dialog"; @@ -60,40 +66,46 @@ void AtomicSwap::runSwap(QStringList arguments){ } else if ( QString confs = line["fields"]["seen_confirmations"].toString(); !confs.isEmpty()){ qDebug() << "Updating xmrconfs " + confs; this->updateXMRConf(confs.toInt()); - } 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]; + } else if (QString::compare(message, "Bitcoin transaction status changed")==0){ QString status = line["fields"]["new_status"].toString(); - bool ok; - auto confirmations = status.split(" ")[2].toInt(&ok, 10); - if(ok) { - this->updateBTCConf(confirmations); - } else { + auto parts = status.split(" "); + if (parts.length() == 2){ this->updateStatus("Found txid " + line["fields"]["txid"].toString() + " in mempool"); + + }else { + auto confirmations = parts[2].toInt(); + this->updateBTCConf(confirmations); } - } else if (QString message = line["fields"]["message"].toString(); message.startsWith("Swap completed")){ + } else if (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){ + } else if (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(); + QStringList past = conf()->get(Config::pendingSwap).toStringList(); for(int i=0;i().id,id)==0) { + if(QString::compare(past[i].split(":")[0],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){ + } else if ( 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); + } else if (QString latest_version = line["fields"]["latest_version"].toString(); !latest_version.isEmpty()){ + QMessageBox::warning(this, "Outdated swap version","A newer version of COMIT xmr-btc swap tool is available, delete current binary and re auto install to upgrade"); + conf()->set(Config::swapVersion,latest_version); + } else if (message.startsWith("Acquiring swap lock") && QString::compare("Resume",line["span"]["method_name"].toString())==0){ + updateStatus("Beginning resumption of previous swap"); + this->show(); + } else if (message.startsWith("Deposit at least")){ + min = message.split(" ")[3]; } } }); @@ -106,6 +118,16 @@ AtomicSwap::~AtomicSwap() { for (const auto& proc : *procList){ proc->kill(); } + if(conf()->get(Config::operatingSystem)=="WINDOWS"){ + (new QProcess)->start("tskill", QStringList{"monero-wallet-rpc"}); + }else { + if (constants::networkType==NetworkType::STAGENET){ + (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() +"/testnet/monero/monero-wallet-rpc"}); + } else { + (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + + "/mainnet/monero/monero-wallet-rpc"}); + } + } } void AtomicSwap::logLine(QString line){ this->update(); diff --git a/src/plugins/atomic/AtomicSwap.h b/src/plugins/atomic/AtomicSwap.h index f4e2f100..d6792e92 100644 --- a/src/plugins/atomic/AtomicSwap.h +++ b/src/plugins/atomic/AtomicSwap.h @@ -34,6 +34,7 @@ public slots: private: Ui::AtomicSwap *ui; QString id; + QString min; AtomicFundDialog* fundDialog; QList>* procList; int btc_confs; diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 416b2961..7604c41a 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -11,7 +11,6 @@ #include "AtomicConfigDialog.h" #include "OfferModel.h" #include "utils/AppData.h" -#include "utils/nodes.h" #include "utils/ColorScheme.h" #include "utils/WebsocketNotifier.h" #include "AtomicFundDialog.h" @@ -137,7 +136,6 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons QStringList arguments; arguments << "--data-base-dir"; arguments << Config::defaultConfigDir().absolutePath(); - // Remove after testing if (constants::networkType==NetworkType::STAGENET){ arguments << "--testnet"; } @@ -149,6 +147,8 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons arguments << "--receive-address"; arguments << xmrReceive; + //Doesn't work cause wallet rpc + /* auto nodes = conf()->get(Config::nodes).toJsonObject(); if (nodes.isEmpty()) { auto jsonData = conf()->get(Config::nodes).toByteArray(); @@ -159,6 +159,7 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons } arguments << "--monero-daemon-address"; arguments << nodes.value("0").toObject()["ws"].toArray()[0].toString(); + */ arguments << "--seller"; arguments << seller; if(conf()->get(Config::proxy).toInt() != Config::Proxy::None) { @@ -235,6 +236,16 @@ void AtomicWidget::clean() { for (const auto& proc : *procList){ proc->kill(); } + if(conf()->get(Config::operatingSystem)=="WINDOWS"){ + (new QProcess)->start("tskill", QStringList{"monero-wallet-rpc"}); + }else { + if (constants::networkType==NetworkType::STAGENET){ + (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() +"/testnet/monero/monero-wallet-rpc"}); + } else { + (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + + "/mainnet/monero/monero-wallet-rpc"}); + } + } } diff --git a/src/plugins/atomic/AtomicWidget.ui b/src/plugins/atomic/AtomicWidget.ui index 212fbbd4..88b14043 100644 --- a/src/plugins/atomic/AtomicWidget.ui +++ b/src/plugins/atomic/AtomicWidget.ui @@ -31,7 +31,7 @@ - bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq + @@ -49,7 +49,7 @@ - 888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H + diff --git a/src/plugins/atomic/History.h b/src/plugins/atomic/History.h deleted file mode 100644 index 99904b72..00000000 --- a/src/plugins/atomic/History.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// Created by dev on 7/29/24. -// - -#ifndef FEATHER_HISTORY_H -#define FEATHER_HISTORY_H - -#include -#include - -struct HistoryEntry { - QDateTime timestamp; - QString id; -}; - -Q_DECLARE_METATYPE(HistoryEntry); -#endif //FEATHER_HISTORY_H diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 7eef8411..29aa7d85 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -149,7 +149,7 @@ static const QHash configStrings = { "/dns4/swap.sethforprivacy.com/tcp/8888/p2p/12D3KooWCULyZKuV9YEkb6BX8FuwajdvktSzmMg4U5ZX2uYZjHeu"}}}, {Config::swapPath, {QS("swapPath"), ""}}, {Config::operatingSystem, {QS("operatingSystem"), OS}}, - {Config::pendingSwap, {QS("pendingSwap"), QVariantList{}}}, + {Config::pendingSwap, {QS("pendingSwap"), QStringList{}}}, {Config::swapVersion, {QS("swapVersion"), "0.13.4"}}, }; From f4d27d56d4c5abe6008ea385c75a084f27f850b7 Mon Sep 17 00:00:00 2001 From: twiddle Date: Sat, 24 Aug 2024 09:37:30 -0400 Subject: [PATCH 20/26] v1.0 Refactor libarchive linking and add to guix manifest Fix toolchain Patch 2 Patch 3 Patch 4 Patch 5 Patch 6 patch 7 Patch 8 Patch 9 Patch 11 --- CMakeLists.txt | 4 ++-- contrib/depends/packages/libarchive.mk | 25 +++++++++++++++++++++++ contrib/depends/packages/packages.mk | 8 ++++---- contrib/depends/toolchain.cmake.in | 3 +++ contrib/guix/manifest.scm | 3 +++ src/CMakeLists.txt | 4 ++-- src/plugins/atomic/AtomicConfigDialog.cpp | 17 +++++++++------ src/plugins/atomic/AtomicSwap.cpp | 2 +- src/plugins/atomic/AtomicWidget.cpp | 1 - 9 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 contrib/depends/packages/libarchive.mk diff --git a/CMakeLists.txt b/CMakeLists.txt index 297ead25..9ba6b1c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,14 +16,14 @@ set(COPYRIGHT_YEAR "2024") set(COPYRIGHT_HOLDERS "The Monero Project") # Configurable options -option(STATIC "Link libraries statically, requires static Qt" OFF) +option(STATIC "Link libraries statically, requires static Qt" ON) option(SELF_CONTAINED "Disable when building Feather for packages" OFF) option(TOR_DIR "Directory containing Tor binaries to embed inside Feather" OFF) option(CHECK_UPDATES "Enable checking for application updates" ON) option(PLATFORM_INSTALLER "Built-in updater fetches installer (windows-only)" OFF) option(USE_DEVICE_TREZOR "Trezor support compilation" ON) option(DONATE_BEG "Prompt donation window every once in a while" OFF) -option(WITH_SCANNER "Enable webcam QR scanner" ON) +option(WITH_SCANNER "Enable webcam QR scanner" OFF) option(STACK_TRACE "Dump stack trace on crash (Linux only)" OFF) # diff --git a/contrib/depends/packages/libarchive.mk b/contrib/depends/packages/libarchive.mk new file mode 100644 index 00000000..2508478c --- /dev/null +++ b/contrib/depends/packages/libarchive.mk @@ -0,0 +1,25 @@ +package=libarchive +$(package)_version=3.7.4 +$(package)_download_path=https://github.com/libarchive/libarchive/releases/download/v$($(package)_version)/ +$(package)_file_name=$(package)-$($(package)_version).tar.xz +$(package)_sha256_hash=f887755c434a736a609cbd28d87ddbfbe9d6a3bb5b703c22c02f6af80a802735 + +define $(package)_config_cmds + CC="$($(package)_cc)" \ + CXX="$($(package)_cxx)" \ + AR="$($(package)_ar)" \ + RANLIB="$($(package)_ranlib)" \ + LIBTOOL="$($(package)_libtool)" \ + LDLAGS="$($(package)_ldflags)" \ + CFLAGS="-fPIE" \ + CXXFLAGS="-fPIE" \ + ./configure --host=$(host) --enable-static --prefix=$(host_prefix) --without-iconv +endef + +define $(package)_build_cmds + $(MAKE) +endef + +define $(package)_stage_cmds + $(MAKE) DESTDIR=$($(package)_staging_dir) install +endef diff --git a/contrib/depends/packages/packages.mk b/contrib/depends/packages/packages.mk index b8e4330c..2d590a8d 100644 --- a/contrib/depends/packages/packages.mk +++ b/contrib/depends/packages/packages.mk @@ -1,12 +1,12 @@ -packages := boost openssl libiconv unbound qrencode libsodium polyseed hidapi protobuf libusb zlib libgpg-error libgcrypt expat libzip zxing-cpp +packages := boost openssl libiconv unbound qrencode libsodium polyseed hidapi protobuf libusb zlib libgpg-error libgcrypt expat libzip zxing-cpp libarchive native_packages := native_qt native_protobuf -linux_packages := eudev libfuse libsquashfuse zstd appimage_runtime +linux_packages := eudev libfuse libsquashfuse zstd appimage_runtime linux_native_packages = x86_64_linux_packages := flatstart -darwin_packages := +darwin_packages := darwin_native_packages = darwin_sdk native_cctools native_libtapi mingw32_packages = @@ -18,4 +18,4 @@ qt_mingw32_packages := qt tor_linux_packages := libevent tor_linux tor_darwin_packages := tor_darwin -tor_mingw32_packages := tor_mingw32 \ No newline at end of file +tor_mingw32_packages := tor_mingw32 diff --git a/contrib/depends/toolchain.cmake.in b/contrib/depends/toolchain.cmake.in index 267b056e..2b5d84f8 100644 --- a/contrib/depends/toolchain.cmake.in +++ b/contrib/depends/toolchain.cmake.in @@ -29,6 +29,9 @@ SET(ENV{PKG_CONFIG_PATH} @prefix@/lib/pkgconfig) SET(TOR_DIR @prefix@/Tor CACHE STRING "Tor dir") SET(TOR_VERSION @tor_version@ CACHE STRING "Tor version") +SET(LibArchive_LIBRARIES @prefix@/lib/libarchive.a) +SET(LibArchive_INCLUDE_DIR @prefix@/include/) + SET(Readline_ROOT_DIR @prefix@) SET(Readline_INCLUDE_DIR @prefix@/include) SET(Readline_LIBRARY @prefix@/lib/libreadline.a) diff --git a/contrib/guix/manifest.scm b/contrib/guix/manifest.scm index db968ac4..990a6870 100644 --- a/contrib/guix/manifest.scm +++ b/contrib/guix/manifest.scm @@ -15,6 +15,7 @@ ((gnu packages gettext) #:select (gettext-minimal)) (gnu packages gperf) ((gnu packages installers) #:select (nsis-x86_64)) + (gnu packages backup) ((gnu packages libusb) #:select (libplist)) ((gnu packages linux) #:select (linux-libre-headers-6.1 util-linux)) (gnu packages llvm) @@ -296,6 +297,8 @@ chain for " target " development.")) python-minimal ;; Git git-minimal + ;; Libarchive + libarchive ;; Xcb xcb-util xcb-util-cursor diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8a9410ce..df137682 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -302,7 +302,7 @@ if (WITH_SCANNER) endif() if (WITH_PLUGIN_ATOMIC) - FIND_PACKAGE(LibArchive REQUIRED) + #FIND_PACKAGE(LibArchive REQUIRED) INCLUDE_DIRECTORIES(${LibArchive_INCLUDE_DIR}) target_link_libraries(feather PRIVATE ${LibArchive_LIBRARIES}) endif() @@ -357,4 +357,4 @@ if(APPLE) file(COPY "${CMAKE_SOURCE_DIR}/src/assets/images/appicons/appicon.icns" DESTINATION "${CMAKE_SOURCE_DIR}/installed/feather.app/Contents/Resources/" ) endif() -qt_finalize_executable(feather) \ No newline at end of file +qt_finalize_executable(feather) diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp index b6270744..8780eb76 100644 --- a/src/plugins/atomic/AtomicConfigDialog.cpp +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -7,10 +7,12 @@ #include #include #include -#include +#ifdef Q_OS_WIN #include - +#else +#include #include +#endif #include #include "utils/config.h" @@ -89,9 +91,10 @@ void AtomicConfigDialog::extract() { swapPath.append("/swapTool"); QFile binaryFile(swapPath); binaryFile.open(QIODevice::WriteOnly); - auto operatingSystem = conf()->get(Config::operatingSystem).toString().toStdString(); - if(strcmp("WIN",operatingSystem.c_str()) == 0) { + //auto operatingSystem = conf()->get(Config::operatingSystem).toString().toStdString(); + //if(strcmp("WIN",operatingSystem.c_str()) == 0) { // UNZIP +#ifdef Q_OS_WIN zip *z = zip_open(tempFile.toStdString().c_str(), 0, 0); //Search for the file of given name @@ -114,7 +117,8 @@ void AtomicConfigDialog::extract() { //And close the archive zip_close(z); conf()->set(Config::swapPath,swapPath); - } else { +#else + //} else { struct archive *a; struct archive *ext; @@ -145,7 +149,8 @@ void AtomicConfigDialog::extract() { archive_write_close(ext); archive_write_free(ext); conf()->set(Config::swapPath, QString(savePath.c_str())); - } +#endif + //} qDebug() << "Finished"; binaryFile.close(); ui->downloadLabel->setText("Swap tool installation complete, Atomic swaps are ready !"); diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp index 23ce35a0..5862ee8f 100644 --- a/src/plugins/atomic/AtomicSwap.cpp +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -7,7 +7,7 @@ #include "AtomicSwap.h" #include -#include +#include #include "ui_AtomicSwap.h" #include "AtomicWidget.h" #include "constants.h" diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 7604c41a..63c86e4c 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -96,7 +96,6 @@ AtomicWidget::AtomicWidget(QWidget *parent) } } - sleep(1); runSwap(seller,btcChange, xmrReceive); } }); From 049a770d9b0cd7706b01e24c29a94fe3ffc7ce17 Mon Sep 17 00:00:00 2001 From: Twiddle <81495002+BrandyJSon@users.noreply.github.com> Date: Tue, 27 Aug 2024 00:32:40 -0400 Subject: [PATCH 21/26] Update README.md Include demo and info about usage --- README.md | 49 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ae618531..bd676838 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,43 @@ -# Feather Wallet +# Feather Atomic -Feather is a free, open-source Monero wallet for Linux, Tails, macOS and Windows. It is written in C++ with the Qt framework. +Feather Atomic is a plugin for the free, open-source Monero wallet for Linux, Tails, macOS and Windows, Feather. It is written in C++ with the Qt framework. -Copyright (c) 2020-2024, The Monero Project. +## Usage +Make sure to click configure button in the atomic tab and either auto install the swap binary or select the already installed binary. +All files relevant to swap will be stored in feathers config directory including the binary if you auto install. +### If after installing the swap tool, refresh fails to show any swaps make sure to check that you have a version of GLIBC greater or equal to 2.32 or this plugin will not be able to run on your system. + +### Running a swap +1. Click refresh offers (may take a little if routing through tor) +2. Click the offer you would like to take from the table +3. Enter a BTC change address (make sure you are using a testnet address if feather is running stagenet mode) +4. Enter a XMR receive address +5. Click swap +6. Wait for fund dialog to appear, send at least the minimum BTC covering tx fees to the address and not more then the max BTC of the offer (if there is BTC from a previous uncompleted swap then this dialog may not appear) +7. Once the transaction is detected in the BTC mempool a different dialog will appear showing the status of confirmations for BTC and XMR side of swap +8. If all goes right after the BTC transaction reaches at least 1 confirmation and the XMR transaction reaches at least 10 confirmations the swap complete and both parties should be happy with their new coins + +### Recovery +If for some reason the swap isn't able to be completed when ran, one party goes offline, application crashes, etc then a recovery dialog will automatically appear next time you launch feather atomic as long as the swap still can be resumed or canceled. +#### To cancel a swap +1. Make sure swap has had enough BTC confirmations to be able to be canceled (should take about 12 hours, gui reflect if it can be canceled) +2. Click the swap you wish to cancel then click the cancel button +3. A dialog will spawn and then after a moment the dialog should update to say the swap has been canceled + +#### To resume a swap +1. Click the swap you wish to resume then click the resume button +2. If the seller is still online then the normal swap dialog will appear +3. The confirmations may not always be accurate when resuming so rely on the status message in the dialog to follow progress of the swap. +4. As long as both parties stay online the swap should continue as normal and end with both parties having their new coins + + +### Testnet4 swap demo + + +https://github.com/user-attachments/assets/b7871a2f-21d0-46e2-b575-b9ba885a810d + +#### This video has been speed up in multiple sections time on the top right is accurate to real life time. ## Resources * Web: [featherwallet.org](https://featherwallet.org) @@ -15,15 +49,6 @@ Copyright (c) 2020-2024, The Monero Project. * IRC: `#feather` on [OFTC](https://www.oftc.net/) * Matrix: [matrix.to/#/#feather:monero.social](https://matrix.to/#/#feather:monero.social) -**Download** the latest release [here](https://featherwallet.org/download). - -## Supporting the project - -Feather is a 100% community-sponsored project. If you want to join our efforts, the easiest thing you can do is support the project financially. - -Donations help pay for hosting, build servers, domain names, e-mail and other recurring costs. Any amount helps. - -`47ntfT2Z5384zku39pTM6hGcnLnvpRYW2Azm87GiAAH2bcTidtq278TL6HmwyL8yjMeERqGEBs3cqC8vvHPJd1cWQrGC65f` ## Deterministic builds From a530b1ca8160b55507e3f6a8e16b6aba10e92fa4 Mon Sep 17 00:00:00 2001 From: twiddle Date: Wed, 11 Sep 2024 16:13:09 -0400 Subject: [PATCH 22/26] Fixes for Issues #1,#2,#3 and additionally fixes CMakeLists allow non guix builds. --- CMakeLists.txt | 4 ++-- src/CMakeLists.txt | 2 +- src/plugins/atomic/AtomicConfigDialog.cpp | 1 - src/plugins/atomic/AtomicSwap.cpp | 1 - src/plugins/atomic/AtomicWidget.cpp | 26 ++++++++++++++++++----- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ba6b1c0..13fd7995 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,12 +16,12 @@ set(COPYRIGHT_YEAR "2024") set(COPYRIGHT_HOLDERS "The Monero Project") # Configurable options -option(STATIC "Link libraries statically, requires static Qt" ON) +option(STATIC "Link libraries statically, requires static Qt" OFF) option(SELF_CONTAINED "Disable when building Feather for packages" OFF) option(TOR_DIR "Directory containing Tor binaries to embed inside Feather" OFF) option(CHECK_UPDATES "Enable checking for application updates" ON) option(PLATFORM_INSTALLER "Built-in updater fetches installer (windows-only)" OFF) -option(USE_DEVICE_TREZOR "Trezor support compilation" ON) +option(USE_DEVICE_TREZOR "Trezor support compilation" OFF) option(DONATE_BEG "Prompt donation window every once in a while" OFF) option(WITH_SCANNER "Enable webcam QR scanner" OFF) option(STACK_TRACE "Dump stack trace on crash (Linux only)" OFF) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index df137682..b9beb2a0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -302,7 +302,7 @@ if (WITH_SCANNER) endif() if (WITH_PLUGIN_ATOMIC) - #FIND_PACKAGE(LibArchive REQUIRED) + FIND_PACKAGE(LibArchive REQUIRED) INCLUDE_DIRECTORIES(${LibArchive_INCLUDE_DIR}) target_link_libraries(feather PRIVATE ${LibArchive_LIBRARIES}) endif() diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp index 8780eb76..5eb3a3af 100644 --- a/src/plugins/atomic/AtomicConfigDialog.cpp +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -136,7 +136,6 @@ void AtomicConfigDialog::extract() { if (r == ARCHIVE_EOF) break; savePath = Config::defaultConfigDir().absolutePath().toStdString().append("/").append(archive_entry_pathname(entry)); - qDebug() << savePath; archive_entry_set_pathname(entry, savePath.c_str()); r = archive_write_header(ext, entry); copy_data(a, ext); diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp index 5862ee8f..72faeb21 100644 --- a/src/plugins/atomic/AtomicSwap.cpp +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -114,7 +114,6 @@ void AtomicSwap::runSwap(QStringList arguments){ qDebug() << "process started"; } AtomicSwap::~AtomicSwap() { - delete ui; for (const auto& proc : *procList){ proc->kill(); } diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 63c86e4c..9011259c 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -132,6 +132,7 @@ void AtomicWidget::showAtomicConfigureDialog() { void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, const QString& xmrReceive) { qDebug() << "starting swap"; + clean(); QStringList arguments; arguments << "--data-base-dir"; arguments << Config::defaultConfigDir().absolutePath(); @@ -173,6 +174,7 @@ void AtomicWidget::list(const QString& rendezvous) { QStringList arguments; arguments << "--data-base-dir"; arguments << Config::defaultConfigDir().absolutePath(); + arguments << "--debug"; if (constants::networkType==NetworkType::STAGENET){ arguments << "--testnet"; } @@ -184,6 +186,7 @@ void AtomicWidget::list(const QString& rendezvous) { } arguments << "--rendezvous-point"; arguments << rendezvous; + qDebug() << "rendezvous point: " + rendezvous; auto *swap = new QProcess(); procList->append(QSharedPointer(swap)); swap->setReadChannel(QProcess::StandardError); @@ -196,13 +199,14 @@ void AtomicWidget::list(const QString& rendezvous) { for(const auto& line : lines){ + qDebug() << line; if(line.contains("status")){ parsedLine = QJsonDocument::fromJson(line.toLocal8Bit(), &parseError ); if (parsedLine["fields"]["status"].toString().contains("Online")){ bool skip = false; auto entry = new OfferEntry(parsedLine["fields"]["price"].toString().split( ' ')[0].toDouble(),parsedLine["fields"]["min_quantity"].toString().split(' ')[0].toDouble(),parsedLine["fields"]["max_quantity"].toString().split(' ')[0].toDouble(), parsedLine["fields"]["address"].toString()); for(const auto& post : *offerList){ - if(std::equal(entry->address.begin(), entry->address.end(),post->address.begin(),post->address.end())) + if(entry->max == 0 || std::equal(entry->address.begin(), entry->address.end(),post->address.begin(),post->address.end())) skip = true; } if (!skip) { @@ -210,6 +214,9 @@ void AtomicWidget::list(const QString& rendezvous) { offerList->append(QSharedPointer(entry)); } } + } else if (line.contains("GLIBC_")){ + QMessageBox::critical(this, "GLIBC outdated", "Upgrade your GLIBC to at least 2.32 to use this tool"); + clean(); } } swap->close(); @@ -233,18 +240,27 @@ AtomicWidget::~AtomicWidget() { void AtomicWidget::clean() { for (const auto& proc : *procList){ - proc->kill(); + proc->kill(); } + auto cleanWallet = new QProcess; + auto cleanSwap = new QProcess; if(conf()->get(Config::operatingSystem)=="WINDOWS"){ - (new QProcess)->start("tskill", QStringList{"monero-wallet-rpc"}); + (cleanWallet)->start("tskill", QStringList{"monero-wallet-rpc"}); + (cleanWallet)->start("tskill", QStringList{"swap"}); }else { if (constants::networkType==NetworkType::STAGENET){ - (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() +"/testnet/monero/monero-wallet-rpc"}); + (cleanWallet)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() +"/testnet/monero/monero-wallet-rpc"}); + (cleanSwap)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + + "/*"}); } else { - (new QProcess)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + + (cleanWallet)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + "/mainnet/monero/monero-wallet-rpc"}); + (cleanSwap)->start("pkill", QStringList{"-f", Config::defaultConfigDir().absolutePath() + + "/*"}); } } + cleanWallet->waitForFinished(); + cleanSwap->waitForFinished(); } From 971e5c4716e1d7303187b8b87156cd4ad8876118 Mon Sep 17 00:00:00 2001 From: twiddle Date: Mon, 7 Oct 2024 18:59:24 -0400 Subject: [PATCH 23/26] Removed dead rendezvous points Added a over clearnet checkbox to force the swap tool into listing sellers without tor. This is a temporary fix till comit xmr<->btc maintainers can update libp2p version. --- src/plugins/atomic/AtomicWidget.cpp | 6 +++++- src/plugins/atomic/AtomicWidget.ui | 30 ++++++++++++++++++++++++----- src/utils/config.cpp | 4 +--- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 9011259c..60d577ee 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -180,9 +180,13 @@ void AtomicWidget::list(const QString& rendezvous) { } arguments << "-j"; arguments << "list-sellers"; - if(conf()->get(Config::proxy).toInt() != Config::Proxy::None) { + //Temporary fix till comit xmr btc updates libp2p to work with modern rendezvous points + if(!ui->btn_clearnet->isChecked() && conf()->get(Config::proxy).toInt() != Config::Proxy::None) { arguments << "--tor-socks5-port"; arguments << conf()->get(Config::socks5Port).toString(); + } else if (ui->btn_clearnet->isChecked()) { + arguments << "--tor-socks5-port"; + arguments << "0"; } arguments << "--rendezvous-point"; arguments << rendezvous; diff --git a/src/plugins/atomic/AtomicWidget.ui b/src/plugins/atomic/AtomicWidget.ui index 88b14043..13568bac 100644 --- a/src/plugins/atomic/AtomicWidget.ui +++ b/src/plugins/atomic/AtomicWidget.ui @@ -62,10 +62,10 @@ - Qt::Orientation::Horizontal + Qt::Horizontal - QSizePolicy::Policy::Expanding + QSizePolicy::Expanding @@ -85,7 +85,7 @@ - Qt::Orientation::Horizontal + Qt::Horizontal @@ -113,17 +113,37 @@ + + + + Swap Over Clearnet + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + - No swap tool configured, click configure to set up + No swap tool configured - Qt::Orientation::Horizontal + Qt::Horizontal diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 29aa7d85..f08d6aeb 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -143,10 +143,8 @@ static const QHash configStrings = { // Atomic {Config::rendezVous, {QS("rendezVous"), QStringList{"/dns4/atomicswaps.majesticbank.at/tcp/8888/p2p/12D3KooWKJUwP45K7fLbwGY1VM5V3U7LseU8EwJiAozUFrq5ihoF", - "/dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o", "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", - "/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs", - "/dns4/swap.sethforprivacy.com/tcp/8888/p2p/12D3KooWCULyZKuV9YEkb6BX8FuwajdvktSzmMg4U5ZX2uYZjHeu"}}}, + "/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs"}}}, {Config::swapPath, {QS("swapPath"), ""}}, {Config::operatingSystem, {QS("operatingSystem"), OS}}, {Config::pendingSwap, {QS("pendingSwap"), QStringList{}}}, From 7ec47fd92983c70ec8191997d06ae1f21dee1750 Mon Sep 17 00:00:00 2001 From: twiddle Date: Sat, 12 Oct 2024 09:42:43 -0400 Subject: [PATCH 24/26] Patch Atomicwidget Add back libarchive to guix package post merge --- contrib/depends/packages/packages.mk | 2 +- src/plugins/atomic/AtomicWidget.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/depends/packages/packages.mk b/contrib/depends/packages/packages.mk index 47805514..be1a6683 100644 --- a/contrib/depends/packages/packages.mk +++ b/contrib/depends/packages/packages.mk @@ -1,4 +1,4 @@ -packages := boost openssl unbound qrencode libsodium polyseed hidapi abseil protobuf libusb zlib libgpg-error libgcrypt expat libzip zxing-cpp +packages := boost openssl unbound qrencode libsodium polyseed hidapi abseil protobuf libusb zlib libgpg-error libgcrypt expat libzip zxing-cpp libarchive native_packages := native_qt native_abseil native_protobuf linux_packages := eudev libfuse libsquashfuse zstd appimage_runtime diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index 60d577ee..addd6c4e 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include "AtomicConfigDialog.h" #include "OfferModel.h" From 4b5800f0e514206bab19a1599ed700d180511244 Mon Sep 17 00:00:00 2001 From: twiddle Date: Mon, 14 Oct 2024 13:14:41 -0400 Subject: [PATCH 25/26] Clean up accidental changes to make PR cleaner --- CMakeLists.txt | 3 +-- contrib/depends/packages/packages.mk | 5 ++--- src/MainWindow.cpp | 1 - src/ReceiveWidget.cpp | 2 +- src/utils/config.h | 2 -- src/wizard/WalletWizard.cpp | 2 -- src/wizard/WalletWizard.h | 3 +-- 7 files changed, 5 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f7cad04..5d1e72bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ option(DONATE_BEG "Prompt donation window every once in a while" OFF) option(WITH_SCANNER "Enable webcam QR scanner" OFF) option(STACK_TRACE "Dump stack trace on crash (Linux only)" OFF) -# +# Plugins option(WITH_PLUGIN_HOME "Include Home tab plugin" ON) option(WITH_PLUGIN_TICKERS "Include Tickers Home plugin" ON) option(WITH_PLUGIN_CROWDFUNDING "Include Crowdfunding Home plugin" ON) @@ -115,7 +115,6 @@ endif() # libzip if(CHECK_UPDATES OR WITH_PLUGIN_ATOMIC) set(ZLIB_USE_STATIC_LIBS "ON") - message("libzip inclueded") find_package(ZLIB REQUIRED) find_path(LIBZIP_INCLUDE_DIRS zip.h) find_library(LIBZIP_LIBRARIES zip) diff --git a/contrib/depends/packages/packages.mk b/contrib/depends/packages/packages.mk index be1a6683..153c051d 100644 --- a/contrib/depends/packages/packages.mk +++ b/contrib/depends/packages/packages.mk @@ -1,12 +1,12 @@ packages := boost openssl unbound qrencode libsodium polyseed hidapi abseil protobuf libusb zlib libgpg-error libgcrypt expat libzip zxing-cpp libarchive native_packages := native_qt native_abseil native_protobuf -linux_packages := eudev libfuse libsquashfuse zstd appimage_runtime +linux_packages := eudev libfuse libsquashfuse zstd appimage_runtime linux_native_packages = x86_64_linux_packages := flatstart -darwin_packages := +darwin_packages := darwin_native_packages = darwin_sdk native_cctools native_libtapi mingw32_packages = @@ -18,4 +18,3 @@ qt_mingw32_packages := qt tor_linux_packages := libevent tor_linux tor_darwin_packages := tor_darwin -tor_mingw32_packages := tor_mingw32 diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 44102ebf..f852263c 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -70,7 +70,6 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa this->restoreGeo(); this->initStatusBar(); - this->initPlugins(); this->initWidgets(); this->initMenu(); diff --git a/src/ReceiveWidget.cpp b/src/ReceiveWidget.cpp index b2b9231f..e9fd4cca 100644 --- a/src/ReceiveWidget.cpp +++ b/src/ReceiveWidget.cpp @@ -246,7 +246,7 @@ void ReceiveWidget::updateQrCode(){ void ReceiveWidget::showQrCodeDialog() { SubaddressRow* row = this->currentEntry(); - + if (!row) return; QString address = this->getAddress(row->getRow()); QrCode qr(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH); QrCodeDialog dialog{this, &qr, "Address"}; diff --git a/src/utils/config.h b/src/utils/config.h index dfccd90d..c5272bfe 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -11,8 +11,6 @@ #include #include - - class Config : public QObject { Q_OBJECT diff --git a/src/wizard/WalletWizard.cpp b/src/wizard/WalletWizard.cpp index 7691913a..760f9e94 100644 --- a/src/wizard/WalletWizard.cpp +++ b/src/wizard/WalletWizard.cpp @@ -38,8 +38,6 @@ WalletWizard::WalletWizard(QWidget *parent) auto networkWebsocketPage = new PageNetworkWebsocket(this); auto menuPage = new PageMenu(&m_wizardFields, m_walletKeysFilesModel, this); auto openWalletPage = new PageOpenWallet(m_walletKeysFilesModel, this); - - auto createWallet = new PageWalletFile(&m_wizardFields , this); auto createWalletSeed = new PageWalletSeed(&m_wizardFields, this); auto walletSetPasswordPage = new PageSetPassword(&m_wizardFields, this); diff --git a/src/wizard/WalletWizard.h b/src/wizard/WalletWizard.h index f2bf501a..1cc705c3 100644 --- a/src/wizard/WalletWizard.h +++ b/src/wizard/WalletWizard.h @@ -82,8 +82,7 @@ class WalletWizard : public QWizard Page_HardwareDevice, Page_NetworkProxy, Page_NetworkWebsocket, - Page_Plugins, - Page_Atomic + Page_Plugins }; explicit WalletWizard(QWidget *parent = nullptr); From e705f433bcd6f0bb4c3309f09f7a712bd39c4e2f Mon Sep 17 00:00:00 2001 From: twiddle Date: Mon, 14 Oct 2024 19:55:19 -0400 Subject: [PATCH 26/26] Fix issues mentioned by tobtoht --- CMakeLists.txt | 6 +- contrib/depends/packages/libarchive.mk | 10 +-- contrib/depends/packages/packages.mk | 1 + contrib/guix/manifest.scm | 2 - external/feather-docs | 1 + src/plugins/PluginRegistry.h | 2 +- src/plugins/atomic/AtomicConfigDialog.cpp | 73 +++++++++++++++++----- src/plugins/atomic/AtomicConfigDialog.h | 10 ++- src/plugins/atomic/AtomicFundDialog.h | 5 +- src/plugins/atomic/AtomicRecoverDialog.cpp | 11 ++-- src/plugins/atomic/AtomicRecoverDialog.h | 9 ++- src/plugins/atomic/AtomicSwap.cpp | 27 +++++--- src/plugins/atomic/AtomicSwap.h | 9 ++- src/plugins/atomic/AtomicWidget.cpp | 22 ++++--- src/plugins/atomic/AtomicWidget.h | 12 ++-- src/plugins/atomic/Offer.h | 5 +- src/plugins/atomic/OfferModel.cpp | 5 +- src/plugins/atomic/OfferModel.h | 5 +- src/utils/config.cpp | 11 +--- 19 files changed, 128 insertions(+), 98 deletions(-) mode change 100644 => 160000 external/feather-docs diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d1e72bc..4200abbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,11 +21,11 @@ cmake_policy(SET CMP0074 NEW) option(STATIC "Link libraries statically, requires static Qt" OFF) option(SELF_CONTAINED "Disable when building Feather for packages" OFF) option(TOR_DIR "Directory containing Tor binaries to embed inside Feather" OFF) -option(CHECK_UPDATES "Enable checking for application updates" ON) +option(CHECK_UPDATES "Enable checking for application updates" OFF) option(PLATFORM_INSTALLER "Built-in updater fetches installer (windows-only)" OFF) -option(USE_DEVICE_TREZOR "Trezor support compilation" OFF) +option(USE_DEVICE_TREZOR "Trezor support compilation" ON) option(DONATE_BEG "Prompt donation window every once in a while" OFF) -option(WITH_SCANNER "Enable webcam QR scanner" OFF) +option(WITH_SCANNER "Enable webcam QR scanner" ON) option(STACK_TRACE "Dump stack trace on crash (Linux only)" OFF) # Plugins diff --git a/contrib/depends/packages/libarchive.mk b/contrib/depends/packages/libarchive.mk index 2508478c..352de62c 100644 --- a/contrib/depends/packages/libarchive.mk +++ b/contrib/depends/packages/libarchive.mk @@ -5,15 +5,7 @@ $(package)_file_name=$(package)-$($(package)_version).tar.xz $(package)_sha256_hash=f887755c434a736a609cbd28d87ddbfbe9d6a3bb5b703c22c02f6af80a802735 define $(package)_config_cmds - CC="$($(package)_cc)" \ - CXX="$($(package)_cxx)" \ - AR="$($(package)_ar)" \ - RANLIB="$($(package)_ranlib)" \ - LIBTOOL="$($(package)_libtool)" \ - LDLAGS="$($(package)_ldflags)" \ - CFLAGS="-fPIE" \ - CXXFLAGS="-fPIE" \ - ./configure --host=$(host) --enable-static --prefix=$(host_prefix) --without-iconv + $($(package)_autoconf) endef define $(package)_build_cmds diff --git a/contrib/depends/packages/packages.mk b/contrib/depends/packages/packages.mk index 153c051d..e5cbd6c9 100644 --- a/contrib/depends/packages/packages.mk +++ b/contrib/depends/packages/packages.mk @@ -18,3 +18,4 @@ qt_mingw32_packages := qt tor_linux_packages := libevent tor_linux tor_darwin_packages := tor_darwin +tor_mingw32_packages := tor_mingw32 diff --git a/contrib/guix/manifest.scm b/contrib/guix/manifest.scm index 2f0edf6a..5e46a671 100644 --- a/contrib/guix/manifest.scm +++ b/contrib/guix/manifest.scm @@ -304,8 +304,6 @@ chain for " target " development.")) python-minimal ;; Git git-minimal - ;; Libarchive - libarchive ;; Xcb xcb-util xcb-util-cursor diff --git a/external/feather-docs b/external/feather-docs deleted file mode 100644 index e69de29b..00000000 diff --git a/external/feather-docs b/external/feather-docs new file mode 160000 index 00000000..c816c53f --- /dev/null +++ b/external/feather-docs @@ -0,0 +1 @@ +Subproject commit c816c53f3a8a65fe0e823faa064bc11f025fd0de diff --git a/src/plugins/PluginRegistry.h b/src/plugins/PluginRegistry.h index e63aef4b..9a6d785f 100644 --- a/src/plugins/PluginRegistry.h +++ b/src/plugins/PluginRegistry.h @@ -36,7 +36,7 @@ class PluginRegistry { } bool isPluginEnabled(const QString &id) { - if (!pluginMap.contains(id) or (QString::compare(id,"atomic")==0 && constants::networkType==NetworkType::TESTNET)) { + if (!pluginMap.contains(id) || (QString::compare(id,"atomic")==0 && constants::networkType==NetworkType::TESTNET)) { return false; } diff --git a/src/plugins/atomic/AtomicConfigDialog.cpp b/src/plugins/atomic/AtomicConfigDialog.cpp index 5eb3a3af..f47e89e1 100644 --- a/src/plugins/atomic/AtomicConfigDialog.cpp +++ b/src/plugins/atomic/AtomicConfigDialog.cpp @@ -5,18 +5,30 @@ #include "ui_AtomicConfigDialog.h" #include -#include #include +#include +#include "utils/config.h" +#include "utils/Networking.h" + #ifdef Q_OS_WIN #include +#define OS 1 // "WINDOWS" #else #include #include +#ifdef Q_PROCESSOR_X86_64 +#define ARCH 1 // "x86_64" +#elifdef Q_PROCESSOR_ARM_V7 +#define ARCH 2 // "armv7") +#else +#define ARCH 3 // "assumes aarch64 or unsupported" +#endif +#ifdef Q_OS_DARWIN +#define OS 2 // "MAC" +#else +#define OS 3 // "LINUX" +#endif #endif -#include - -#include "utils/config.h" -#include "utils/Networking.h" AtomicConfigDialog::AtomicConfigDialog(QWidget *parent) : WindowModalDialog(parent) @@ -40,7 +52,7 @@ AtomicConfigDialog::AtomicConfigDialog(QWidget *parent) QString path = QFileDialog::getOpenFileName(this, "Select swap binary file", Config::defaultConfigDir().absolutePath(), "Binary Executable (*)"); - Config::instance()->set(Config::swapPath, path); + saveSwapPath(path); if(path.isEmpty()){ return; } @@ -53,21 +65,50 @@ AtomicConfigDialog::AtomicConfigDialog(QWidget *parent) this->adjustSize(); } +QString AtomicConfigDialog::getPath() { + QFile* pathFile = new QFile(Config::defaultConfigDir().absolutePath() +"/swapPath.conf"); + pathFile->open(QIODevice::ReadOnly); + QString toolPath = pathFile->readAll(); + pathFile->close(); + return toolPath; +} + +void AtomicConfigDialog::saveSwapPath(QString path) { + QFile* pathFile = new QFile(Config::defaultConfigDir().absolutePath() +"/swapPath.conf"); + pathFile->open(QIODevice::WriteOnly); + pathFile->resize(0); + pathFile->write(path.toStdString().c_str()); + pathFile->close(); +} void AtomicConfigDialog::downloadBinary() { auto* network = new Networking(this); download = new QTemporaryFile(this); download->open(); tempFile = download->fileName(); QString url; - 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) { + QString swapVersion = "0.13.4"; + QString firstPart = "https://github.com/UnstoppableSwap/core/releases/download/" + swapVersion; + if(OS == 1) { // HARD CODED DOWNload URL CHANGE IF PROBLEMS - url = QString(firstPart+"/swap_"+conf()->get(Config::swapVersion).toString()+"_Windows_x86_64.zip"); - } else if (strcmp("LINUX",operatingSystem.c_str())==0){ - url = QString(firstPart+"/swap_"+conf()->get(Config::swapVersion).toString()+"_Linux_x86_64.tar"); + url = QString(firstPart+"/swap_"+ swapVersion + "_Windows_x86_64.zip"); + } else if (OS == 3){ + if (ARCH == 1) { + url = QString(firstPart+"/swap_"+ swapVersion + "_Linux_x86_64.tar"); + } else if (ARCH == 2) { + url = QString(firstPart+"/swap_"+ swapVersion + "_Linux_armv7.tar"); + } else { + qDebug() << "Unsupported architecture"; + throw std::runtime_error("Unsupported architecture"); + } } else { - url = QString(firstPart + "/swap_" + conf()->get(Config::swapVersion).toString() + "_Darwin_x86_64.tar"); + if (ARCH == 1) { + url = QString(firstPart + "/swap_"+ swapVersion + "_Darwin_x86_64.tar"); + } else if (ARCH == 3) { + url = QString(firstPart + "/swap_"+ swapVersion + "_Darwin_aarch64.tar"); + } else { + qDebug() << "Unsupported architecture"; + throw std::runtime_error("Unsupported architecture"); + } } archive = network->get(this, url); @@ -88,7 +129,7 @@ void AtomicConfigDialog::extract() { archive->deleteLater(); auto swapPath = Config::defaultConfigDir().absolutePath(); - swapPath.append("/swapTool"); + swapPath.append("/swap"); QFile binaryFile(swapPath); binaryFile.open(QIODevice::WriteOnly); //auto operatingSystem = conf()->get(Config::operatingSystem).toString().toStdString(); @@ -116,7 +157,7 @@ void AtomicConfigDialog::extract() { zip_fclose(f); //And close the archive zip_close(z); - conf()->set(Config::swapPath,swapPath); + saveSwapPath(swapPath+".exe"); #else //} else { @@ -147,7 +188,7 @@ void AtomicConfigDialog::extract() { archive_write_close(ext); archive_write_free(ext); - conf()->set(Config::swapPath, QString(savePath.c_str())); + saveSwapPath(swapPath); #endif //} qDebug() << "Finished"; diff --git a/src/plugins/atomic/AtomicConfigDialog.h b/src/plugins/atomic/AtomicConfigDialog.h index 69023677..6ac7a85e 100644 --- a/src/plugins/atomic/AtomicConfigDialog.h +++ b/src/plugins/atomic/AtomicConfigDialog.h @@ -23,6 +23,7 @@ Q_OBJECT public: explicit AtomicConfigDialog(QWidget *parent = nullptr); ~AtomicConfigDialog() override; + static QString getPath(); public slots: void extract(); @@ -30,14 +31,11 @@ public slots: private: void downloadBinary(); int copy_data(struct archive *ar, struct archive *aw); - + void saveSwapPath(QString path); QScopedPointer ui; - - QNetworkReply* archive; + QNetworkReply* archive = nullptr; QString tempFile; - QTemporaryFile* download; - - + QTemporaryFile* download = nullptr; }; diff --git a/src/plugins/atomic/AtomicFundDialog.h b/src/plugins/atomic/AtomicFundDialog.h index f9dee47f..53684d50 100644 --- a/src/plugins/atomic/AtomicFundDialog.h +++ b/src/plugins/atomic/AtomicFundDialog.h @@ -1,6 +1,5 @@ -// -// Created by dev on 7/8/24. -// +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project #ifndef FEATHER_ATOMICFUNDDIALOG_H #define FEATHER_ATOMICFUNDDIALOG_H diff --git a/src/plugins/atomic/AtomicRecoverDialog.cpp b/src/plugins/atomic/AtomicRecoverDialog.cpp index 8f894043..f6613838 100644 --- a/src/plugins/atomic/AtomicRecoverDialog.cpp +++ b/src/plugins/atomic/AtomicRecoverDialog.cpp @@ -1,6 +1,5 @@ -// -// Created by dev on 7/29/24. -// +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project // You may need to build the project (run Qt uic code generator) to get "ui_AtomicRecoverDialog.h" resolved @@ -40,11 +39,11 @@ AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : QDateTime timestamp = QDateTime::fromString(entry[1],"dd.MM.yyyy.hh.mm.ss"); qint64 difference = timestamp.secsTo(QDateTime::currentDateTime()); - if (difference < 86400) { + if (difference < 86400) { // 86400 is number of seconds in a day (if a swap is older it is punished) rowData.clear(); rowData << new QStandardItem(id); rowData << new QStandardItem(timestamp.toString("MM-dd-yyyy hh:mm")); - if (difference > 43200){ + if (difference > 43200){ // 43200 is number of seconds in 12 hours rowData << new QStandardItem("Refundable"); } else rowData << new QStandardItem("Recoverable/Pending Refund Timelock"); @@ -73,7 +72,7 @@ AtomicRecoverDialog::AtomicRecoverDialog(QWidget *parent) : arguments << "--swap-id"; auto row = ui->swap_history->selectionModel()->selectedRows().at(0); arguments << row.sibling(row.row(),0).data().toString(); - if(conf()->get(Config::proxy).toInt() != Config::Proxy::None) { + if(conf()->get(Config::proxy).toInt() == Config::Proxy::Tor) { arguments << "--tor-socks5-port"; arguments << conf()->get(Config::socks5Port).toString(); } diff --git a/src/plugins/atomic/AtomicRecoverDialog.h b/src/plugins/atomic/AtomicRecoverDialog.h index 29c871f3..447d07ba 100644 --- a/src/plugins/atomic/AtomicRecoverDialog.h +++ b/src/plugins/atomic/AtomicRecoverDialog.h @@ -1,6 +1,5 @@ -// -// Created by dev on 7/29/24. -// +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project #ifndef FEATHER_ATOMICRECOVERDIALOG_H #define FEATHER_ATOMICRECOVERDIALOG_H @@ -25,8 +24,8 @@ Q_OBJECT private slots: void updateBtn(const QModelIndex &index); private: - Ui::AtomicRecoverDialog *ui; - AtomicSwap *swapDialog; + Ui::AtomicRecoverDialog *ui = nullptr; + AtomicSwap *swapDialog = nullptr; }; diff --git a/src/plugins/atomic/AtomicSwap.cpp b/src/plugins/atomic/AtomicSwap.cpp index 72faeb21..3d517d76 100644 --- a/src/plugins/atomic/AtomicSwap.cpp +++ b/src/plugins/atomic/AtomicSwap.cpp @@ -1,6 +1,5 @@ -// -// Created by dev on 6/11/24. -// +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project // You may need to build the project (run Qt uic code generator) to get "ui_AtomicSwap.h" resolved @@ -8,11 +7,17 @@ #include #include + +#include "AtomicConfigDialog.h" #include "ui_AtomicSwap.h" #include "AtomicWidget.h" #include "constants.h" #include "networktype.h" - +#ifdef QT_OS_WIN +#define WINDOWS 1 +#else +#define WINDOWS 0 +#endif AtomicSwap::AtomicSwap(QWidget *parent) : WindowModalDialog(parent), ui(new Ui::AtomicSwap), fundDialog( new AtomicFundDialog(this)), procList(new QList>()) { @@ -38,6 +43,13 @@ void AtomicSwap::runSwap(QStringList arguments){ QJsonParseError err; const QByteArray& rawline = swap->readLine(); QJsonDocument line = QJsonDocument::fromJson(rawline, &err); + if (err.error != QJsonParseError::NoError) { + qDebug()<close(); + this->close(); + return; + } qDebug() << rawline; bool check; QString message = line["fields"]["message"].toString(); @@ -99,8 +111,7 @@ void AtomicSwap::runSwap(QStringList arguments){ 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 latest_version = line["fields"]["latest_version"].toString(); !latest_version.isEmpty()){ - QMessageBox::warning(this, "Outdated swap version","A newer version of COMIT xmr-btc swap tool is available, delete current binary and re auto install to upgrade"); - conf()->set(Config::swapVersion,latest_version); + QMessageBox::warning(this, "Outdated swap version","A newer version of COMIT xmr-btc swap tool is available"); } else if (message.startsWith("Acquiring swap lock") && QString::compare("Resume",line["span"]["method_name"].toString())==0){ updateStatus("Beginning resumption of previous swap"); this->show(); @@ -110,14 +121,14 @@ void AtomicSwap::runSwap(QStringList arguments){ } }); - swap->start(conf()->get(Config::swapPath).toString(),arguments); + swap->start(AtomicConfigDialog::getPath(),arguments); qDebug() << "process started"; } AtomicSwap::~AtomicSwap() { for (const auto& proc : *procList){ proc->kill(); } - if(conf()->get(Config::operatingSystem)=="WINDOWS"){ + if(WINDOWS){ (new QProcess)->start("tskill", QStringList{"monero-wallet-rpc"}); }else { if (constants::networkType==NetworkType::STAGENET){ diff --git a/src/plugins/atomic/AtomicSwap.h b/src/plugins/atomic/AtomicSwap.h index d6792e92..553c8ea5 100644 --- a/src/plugins/atomic/AtomicSwap.h +++ b/src/plugins/atomic/AtomicSwap.h @@ -1,6 +1,5 @@ -// -// Created by dev on 6/11/24. -// +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project #ifndef FEATHER_ATOMICSWAP_H #define FEATHER_ATOMICSWAP_H @@ -32,11 +31,11 @@ public slots: signals: void cleanProcs(); private: - Ui::AtomicSwap *ui; + Ui::AtomicSwap *ui = nullptr; QString id; QString min; AtomicFundDialog* fundDialog; - QList>* procList; + QList>* procList = nullptr; int btc_confs; void cancel(); diff --git a/src/plugins/atomic/AtomicWidget.cpp b/src/plugins/atomic/AtomicWidget.cpp index addd6c4e..d0fee966 100644 --- a/src/plugins/atomic/AtomicWidget.cpp +++ b/src/plugins/atomic/AtomicWidget.cpp @@ -9,13 +9,19 @@ #include #include #include - #include "AtomicConfigDialog.h" #include "OfferModel.h" #include "utils/AppData.h" #include "utils/ColorScheme.h" #include "utils/WebsocketNotifier.h" #include "AtomicFundDialog.h" +#include "WalletManager.h" + +#ifdef QT_OS_WIN +#define WINDOWS 1 +#else +#define WINDOWS 0 +#endif AtomicWidget::AtomicWidget(QWidget *parent) : QWidget(parent) @@ -76,14 +82,12 @@ AtomicWidget::AtomicWidget(QWidget *parent) 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()){ + if(WalletManager::addressValid(xmrReceive,NetworkType::STAGENET)){ QMessageBox::warning(this, "Warning","XMR receive address is improperly formated or on wrong network"); return; } @@ -92,7 +96,7 @@ AtomicWidget::AtomicWidget(QWidget *parent) QMessageBox::warning(this, "Warning","BTC change address is wrong, not a bech32 segwit address,or on wrong network"); return; } - if(!xmrMain.match(xmrReceive).hasMatch()){ + if(WalletManager::addressValid(xmrReceive,NetworkType::MAINNET)){ QMessageBox::warning(this, "Warning","XMR receive address is improperly formated or on wrong network"); return; } @@ -164,7 +168,7 @@ void AtomicWidget::runSwap(const QString& seller, const QString& btcChange, cons */ arguments << "--seller"; arguments << seller; - if(conf()->get(Config::proxy).toInt() != Config::Proxy::None) { + if(conf()->get(Config::proxy).toInt() == Config::Proxy::Tor) { arguments << "--tor-socks5-port"; arguments << conf()->get(Config::socks5Port).toString(); } @@ -183,7 +187,7 @@ void AtomicWidget::list(const QString& rendezvous) { arguments << "-j"; arguments << "list-sellers"; //Temporary fix till comit xmr btc updates libp2p to work with modern rendezvous points - if(!ui->btn_clearnet->isChecked() && conf()->get(Config::proxy).toInt() != Config::Proxy::None) { + if(!ui->btn_clearnet->isChecked() && conf()->get(Config::proxy).toInt() == Config::Proxy::Tor) { arguments << "--tor-socks5-port"; arguments << conf()->get(Config::socks5Port).toString(); } else if (ui->btn_clearnet->isChecked()) { @@ -229,7 +233,7 @@ void AtomicWidget::list(const QString& rendezvous) { o_model->updateOffers(*offerList); return list; }); - swap->start(conf()->get(Config::swapPath).toString(), arguments); + swap->start(AtomicConfigDialog::getPath(), arguments); @@ -250,7 +254,7 @@ void AtomicWidget::clean() { } auto cleanWallet = new QProcess; auto cleanSwap = new QProcess; - if(conf()->get(Config::operatingSystem)=="WINDOWS"){ + if(WINDOWS){ (cleanWallet)->start("tskill", QStringList{"monero-wallet-rpc"}); (cleanWallet)->start("tskill", QStringList{"swap"}); }else { diff --git a/src/plugins/atomic/AtomicWidget.h b/src/plugins/atomic/AtomicWidget.h index c465ced7..32b27dad 100644 --- a/src/plugins/atomic/AtomicWidget.h +++ b/src/plugins/atomic/AtomicWidget.h @@ -41,13 +41,13 @@ private slots: QScopedPointer ui; bool m_comboBoxInit = false; QTimer m_statusTimer; - OfferModel *o_model; - QList> *offerList; - AtomicSwap *swapDialog; - AtomicFundDialog *fundDialog; - AtomicRecoverDialog *recoverDialog; + OfferModel *o_model = nullptr; + QList> *offerList = nullptr; + AtomicSwap *swapDialog = nullptr; + AtomicFundDialog *fundDialog = nullptr; + AtomicRecoverDialog *recoverDialog = nullptr; - QList> *procList; + QList> *procList = nullptr; }; #endif // FEATHER_ATOMICWIDGET_H diff --git a/src/plugins/atomic/Offer.h b/src/plugins/atomic/Offer.h index 09400416..804ccd6e 100644 --- a/src/plugins/atomic/Offer.h +++ b/src/plugins/atomic/Offer.h @@ -1,6 +1,5 @@ -// -// Created by dev on 5/23/24. -// +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project #ifndef FEATHER_OFFER_H #define FEATHER_OFFER_H diff --git a/src/plugins/atomic/OfferModel.cpp b/src/plugins/atomic/OfferModel.cpp index 013a0806..1197d433 100644 --- a/src/plugins/atomic/OfferModel.cpp +++ b/src/plugins/atomic/OfferModel.cpp @@ -1,6 +1,5 @@ -// -// Created by dev on 5/23/24. -// +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project #include "OfferModel.h" diff --git a/src/plugins/atomic/OfferModel.h b/src/plugins/atomic/OfferModel.h index d6aded3f..00626315 100644 --- a/src/plugins/atomic/OfferModel.h +++ b/src/plugins/atomic/OfferModel.h @@ -1,6 +1,5 @@ -// -// Created by dev on 5/23/24. -// +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project #ifndef FEATHER_OFFERMODEL_H #define FEATHER_OFFERMODEL_H diff --git a/src/utils/config.cpp b/src/utils/config.cpp index d3dae220..0a976c11 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -12,13 +12,7 @@ #include "utils/os/tails.h" #define QS QStringLiteral -#if defined(Q_OS_WIN64) -#define OS "WINDOWS" -#elif defined(Q_OS_DARWIN) -#define OS "MAC" -#else -#define OS "LINUX" -#endif + struct ConfigDirective { QString name; @@ -150,10 +144,7 @@ static const QHash configStrings = { {Config::rendezVous, {QS("rendezVous"), QStringList{"/dns4/atomicswaps.majesticbank.at/tcp/8888/p2p/12D3KooWKJUwP45K7fLbwGY1VM5V3U7LseU8EwJiAozUFrq5ihoF", "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", "/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs"}}}, - {Config::swapPath, {QS("swapPath"), ""}}, - {Config::operatingSystem, {QS("operatingSystem"), OS}}, {Config::pendingSwap, {QS("pendingSwap"), QStringList{}}}, - {Config::swapVersion, {QS("swapVersion"), "0.13.4"}}, };