diff --git a/src/appcontext.cpp b/src/appcontext.cpp index a4bce3f..eaf3f3a 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -23,6 +23,7 @@ QMap AppContext::txDescriptionCache; QMap AppContext::txCache; AppContext::AppContext(QCommandLineParser *cmdargs) { + this->m_walletKeysFilesModel = new WalletKeysFilesModel(this, this); this->network = new QNetworkAccessManager(); this->networkClearnet = new QNetworkAccessManager(); this->cmdargs = cmdargs; @@ -193,7 +194,7 @@ void AppContext::onSweepOutput(const QString &keyImage, QString address, bool ch this->currentWallet->createTransactionSingleAsync(keyImage, address, outputs, this->tx_priority); } -void AppContext::onCreateTransaction(const QString &address, quint64 amount, const QString &description, bool all) { +void AppContext::onCreateTransaction(QString address, quint64 amount, QString description, bool all) { // tx creation this->tmpTxDescription = description; @@ -342,7 +343,7 @@ void AppContext::onWalletOpened(Wallet *wallet) { connect(this->currentWallet, &Wallet::heightRefreshed, this, &AppContext::onHeightRefreshed); connect(this->currentWallet, &Wallet::transactionCreated, this, &AppContext::onTransactionCreated); - emit walletOpened(); + emit walletOpened(wallet); connect(this->currentWallet, &Wallet::connectionStatusChanged, [this]{ this->nodes->autoConnect(); @@ -770,6 +771,31 @@ void AppContext::onTransactionCreated(PendingTransaction *tx, const QVectorstatus(); + auto err = QString("Can't create transaction: "); + + if(tx_status != PendingTransaction::Status_Ok){ + auto tx_err = tx->errorString(); + qCritical() << tx_err; + + if (this->currentWallet->connectionStatus() == Wallet::ConnectionStatus_WrongVersion) + err = QString("%1 Wrong daemon version: %2").arg(err).arg(tx_err); + else + err = QString("%1 %2").arg(err).arg(tx_err); + + qDebug() << Q_FUNC_INFO << err; + emit createTransactionError(err); + this->currentWallet->disposeTransaction(tx); + return; + } else if (tx->txCount() == 0) { + err = QString("%1 %2").arg(err).arg("No unmixable outputs to sweep."); + qDebug() << Q_FUNC_INFO << err; + emit createTransactionError(err); + this->currentWallet->disposeTransaction(tx); + return; + } + // tx created, but not sent yet. ask user to verify first. emit createTransactionSuccess(tx, address); } diff --git a/src/appcontext.h b/src/appcontext.h index 6b96557..cc69167 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -89,6 +89,12 @@ public: static QMap txCache; static TxFiatHistory *txFiatHistory; + QList listWallets() { + // return listing of wallet .keys items + m_walletKeysFilesModel->refresh(); + return m_walletKeysFilesModel->listWallets(); + } + // libwalletqt bool refreshed = false; WalletManager *walletManager; @@ -111,7 +117,7 @@ public: public slots: void onOpenWallet(const QString& path, const QString &password); - void onCreateTransaction(const QString &address, quint64 amount, const QString &description, bool all); + void onCreateTransaction(QString address, quint64 amount, const QString description, bool all); void onCreateTransactionMultiDest(const QVector &addresses, const QVector &amounts, const QString &description); void onCancelTransaction(PendingTransaction *tx, const QVector &address); void onSweepOutput(const QString &keyImage, QString address, bool churn, int outputs) const; @@ -150,7 +156,7 @@ signals: void synchronized(); void blockHeightWSUpdated(QMap heights); void walletRefreshed(); - void walletOpened(); + void walletOpened(Wallet *wallet); void walletCreatedError(const QString &msg); void walletCreated(Wallet *wallet); void walletOpenedError(QString msg); @@ -177,6 +183,7 @@ signals: void setTitle(const QString &title); // set window title private: + WalletKeysFilesModel *m_walletKeysFilesModel; const int m_donationBoundary = 15; QTimer m_storeTimer; QUrl m_wsUrl = QUrl(QStringLiteral("ws://feathercitimllbmdktu6cmjo3fizgmyfrntntqzu6xguqa2rlq5cgid.onion/ws")); diff --git a/src/cli.cpp b/src/cli.cpp index 8384db9..80a90e3 100644 --- a/src/cli.cpp +++ b/src/cli.cpp @@ -1,13 +1,13 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2020-2021, The Monero Project. -#include "cli.h" - // libwalletqt #include "libwalletqt/TransactionHistory.h" #include "model/AddressBookModel.h" #include "model/TransactionHistoryModel.h" +#include "cli.h" + CLI::CLI(AppContext *ctx, QObject *parent) : QObject(parent), ctx(ctx) { @@ -22,6 +22,8 @@ void CLI::run() { if(!ctx->cmdargs->isSet("wallet-file")) return this->finishedError("--wallet-file argument missing"); if(!ctx->cmdargs->isSet("password")) return this->finishedError("--password argument missing"); ctx->onOpenWallet(ctx->cmdargs->value("wallet-file"), ctx->cmdargs->value("password")); + } else if(mode == CLIMode::CLIDaemonize) { + m_wsServer = new WSServer(ctx, QHostAddress(this->backgroundWebsocketAddress), this->backgroundWebsocketPort, this->backgroundWebsocketPassword, true, this); } } diff --git a/src/cli.h b/src/cli.h index 24b538e..c946381 100644 --- a/src/cli.h +++ b/src/cli.h @@ -6,10 +6,12 @@ #include #include "appcontext.h" +#include enum CLIMode { CLIModeExportContacts, - CLIModeExportTxHistory + CLIModeExportTxHistory, + CLIDaemonize }; class CLI : public QObject @@ -20,6 +22,10 @@ public: explicit CLI(AppContext *ctx, QObject *parent = nullptr); ~CLI() override; + QString backgroundWebsocketAddress; + quint16 backgroundWebsocketPort; + QString backgroundWebsocketPassword; + public slots: void run(); @@ -30,6 +36,7 @@ public slots: private: AppContext *ctx; + WSServer *m_wsServer; private slots: void finished(const QString &msg); diff --git a/src/libwalletqt/AddressBook.cpp b/src/libwalletqt/AddressBook.cpp index 381d763..6b485b0 100644 --- a/src/libwalletqt/AddressBook.cpp +++ b/src/libwalletqt/AddressBook.cpp @@ -109,6 +109,25 @@ bool AddressBook::deleteRow(int rowId) return result; } +bool AddressBook::deleteByAddress(const QString &address) { + bool result; + QWriteLocker locker(&m_lock); + + const QMap::const_iterator it = m_addresses.find(address); + if (it == m_addresses.end()) + return false; + + { + result = m_addressBookImpl->deleteRow(*it); + } + + // Fetch new data from wallet2. + if (result) + getAll(); + + return result; +} + quint64 AddressBook::count() const { QReadLocker locker(&m_lock); diff --git a/src/libwalletqt/AddressBook.h b/src/libwalletqt/AddressBook.h index 56d7827..e1852de 100644 --- a/src/libwalletqt/AddressBook.h +++ b/src/libwalletqt/AddressBook.h @@ -24,6 +24,7 @@ public: Q_INVOKABLE bool getRow(int index, std::function callback) const; Q_INVOKABLE bool addRow(const QString &address, const QString &payment_id, const QString &description); Q_INVOKABLE bool deleteRow(int rowId); + Q_INVOKABLE bool deleteByAddress(const QString &description); Q_INVOKABLE void setDescription(int index, const QString &label); quint64 count() const; Q_INVOKABLE QString errorString() const; diff --git a/src/libwalletqt/TransactionHistory.cpp b/src/libwalletqt/TransactionHistory.cpp index d804dc4..a4f4432 100644 --- a/src/libwalletqt/TransactionHistory.cpp +++ b/src/libwalletqt/TransactionHistory.cpp @@ -202,3 +202,74 @@ bool TransactionHistory::writeCSV(const QString &path) { data = QString("blockHeight,epoch,date,direction,amount,fiat,atomicAmount,fee,txid,label,subaddrAccount,paymentId\n%1").arg(data); return Utils::fileWrite(path, data); } + +QJsonArray TransactionHistory::toJsonArray(){ + QJsonArray return_array; + + for (const auto &tx : m_pimpl->getAll()) { + if (tx->subaddrAccount() != 0) { // only account 0 + continue; + } + + TransactionInfo info(tx, this); + + // collect column data + QDateTime timeStamp = info.timestamp(); + double amount = info.amount(); + + // calc historical fiat price + QString fiatAmount; + QString preferredCur = config()->get(Config::preferredFiatCurrency).toString(); + const double usd_price = AppContext::txFiatHistory->get(timeStamp.toString("yyyyMMdd")); + double fiat_price = usd_price * amount; + + if(preferredCur != "USD") + fiat_price = AppContext::prices->convert("USD", preferredCur, fiat_price); + double fiat_rounded = ceil(Utils::roundSignificant(fiat_price, 3) * 100.0) / 100.0; + if(fiat_price != 0) + fiatAmount = QString("%1 %2").arg(QString::number(fiat_rounded)).arg(preferredCur); + + // collect some more column data + quint64 atomicAmount = info.atomicAmount(); + quint32 subaddrAccount = info.subaddrAccount(); + QString fee = info.fee(); + QString direction = QString(""); + TransactionInfo::Direction _direction = info.direction(); + if(_direction == TransactionInfo::Direction_In) + direction = QString("in"); + else if(_direction == TransactionInfo::Direction_Out) + direction = QString("out"); + else + continue; // skip TransactionInfo::Direction_Both + + QString label = info.label(); + quint64 blockHeight = info.blockHeight(); + QString date = info.date() + " " + info.time(); + uint epoch = timeStamp.toTime_t(); + QString displayAmount = info.displayAmount(); + QString paymentId = info.paymentId(); + if(paymentId == "0000000000000000") + paymentId = ""; + + QJsonObject tx_item; + tx_item["timestamp"] = (int) epoch; + tx_item["date"] = date; + tx_item["preferred_currency"] = preferredCur; + tx_item["direction"] = direction; + tx_item["blockheight"] = (int) blockHeight; + tx_item["description"] = label; + tx_item["subaddress_account"] = (int) subaddrAccount; + tx_item["payment_id"] = paymentId; + + tx_item["amount"] = amount; + tx_item["amount_display"] = displayAmount; + tx_item["amount_fiat"] = fiatAmount; + tx_item["fiat_rounded"] = fiat_rounded; + tx_item["fiat_price"] = fiat_price; + tx_item["fee"] = fee; + + return_array.append(tx_item); + } + + return return_array; +} \ No newline at end of file diff --git a/src/libwalletqt/TransactionHistory.h b/src/libwalletqt/TransactionHistory.h index 83c4097..cd7fbbf 100644 --- a/src/libwalletqt/TransactionHistory.h +++ b/src/libwalletqt/TransactionHistory.h @@ -32,6 +32,7 @@ public: Q_INVOKABLE void refresh(quint32 accountIndex); Q_INVOKABLE void setTxNote(const QString &txid, const QString ¬e); Q_INVOKABLE bool writeCSV(const QString &path); + Q_INVOKABLE QJsonArray toJsonArray(); quint64 count() const; QDateTime firstDateTime() const; QDateTime lastDateTime() const; diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 052e626..8c94c48 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -1206,6 +1206,18 @@ quint64 Wallet::getBytesSent() const { return m_walletImpl->getBytesSent(); } +QJsonObject Wallet::toJsonObject() { + QJsonObject obj; + obj["path"] = path(); + obj["password"] = getPassword(); + obj["address"] = address(0, 0); + obj["seed"] = getSeed(); + obj["seedLanguage"] = getSeedLanguage(); + obj["networkType"] = nettype(); + obj["walletCreationHeight"] = (int) getWalletCreationHeight(); + return obj; +} + void Wallet::onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort) { if (m_walletListener != nullptr) diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index d48f0b4..5098282 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -450,6 +450,9 @@ public: Q_INVOKABLE quint64 getBytesReceived() const; Q_INVOKABLE quint64 getBytesSent() const; + // return as json object + QJsonObject toJsonObject(); + // TODO: setListenter() when it implemented in API signals: // emitted on every event happened with wallet diff --git a/src/main.cpp b/src/main.cpp index aaeea26..04bbec9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -78,6 +78,12 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { QCommandLineOption exportTxHistoryOption(QStringList() << "export-txhistory", "Output wallet transaction history as CSV to specified path.", "file"); parser.addOption(exportTxHistoryOption); + QCommandLineOption backgroundOption(QStringList() << "daemon", "Start Feather in the background and start a websocket server (IPv4:port)", "backgroundAddress"); + parser.addOption(backgroundOption); + + QCommandLineOption backgroundPasswordOption(QStringList() << "daemon-password", "Password for connecting to the wowlet websocket service", "backgroundPassword"); + parser.addOption(backgroundPasswordOption); + auto parsed = parser.parse(argv_); if(!parsed) { qCritical() << parser.errorText(); @@ -92,7 +98,10 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { bool quiet = parser.isSet(quietModeOption); bool exportContacts = parser.isSet(exportContactsOption); bool exportTxHistory = parser.isSet(exportTxHistoryOption); - bool cliMode = exportContacts || exportTxHistory; + bool backgroundAddressEnabled = parser.isSet(backgroundOption); + bool cliMode = exportContacts || exportTxHistory || backgroundAddressEnabled; + + qRegisterMetaType>(); if(cliMode) { QCoreApplication cli_app(argc, argv); @@ -116,6 +125,27 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { if(!quiet) qInfo() << "CLI mode: Transaction history export"; cli->mode = CLIMode::CLIModeExportTxHistory; + QTimer::singleShot(0, cli, &CLI::run); + } else if(backgroundAddressEnabled) { + if(!quiet) + qInfo() << "CLI mode: daemonize"; + cli->mode = CLIMode::CLIDaemonize; + + auto backgroundHostPort = parser.value(backgroundOption); + if(!backgroundHostPort.contains(":")) { + qCritical() << "the format is: --background ipv4:port"; + return 1; + } + + auto spl = backgroundHostPort.split(":"); + cli->backgroundWebsocketAddress = spl.at(0); + cli->backgroundWebsocketPort = (quint16) spl.at(1).toInt(); + cli->backgroundWebsocketPassword = parser.value(backgroundPasswordOption); + if(cli->backgroundWebsocketPassword.isEmpty()) { + qCritical() << "--daemon-password needs to be set when using --daemon"; + return 1; + } + QTimer::singleShot(0, cli, &CLI::run); } @@ -161,7 +191,6 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { #endif qInstallMessageHandler(Utils::applicationLogHandler); - qRegisterMetaType>(); auto *mainWindow = new MainWindow(ctx); return QApplication::exec(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9468aba..256ff27 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -139,7 +139,7 @@ MainWindow::MainWindow(AppContext *ctx, QWidget *parent) : ui->fiatTickerLayout->addWidget(m_balanceWidget); // Send widget - connect(ui->sendWidget, &SendWidget::createTransaction, m_ctx, QOverload::of(&AppContext::onCreateTransaction)); + connect(ui->sendWidget, &SendWidget::createTransaction, m_ctx, QOverload::of(&AppContext::onCreateTransaction)); connect(ui->sendWidget, &SendWidget::createTransactionMultiDest, m_ctx, &AppContext::onCreateTransactionMultiDest); // Nodes @@ -578,7 +578,7 @@ void MainWindow::onWalletCreated(Wallet *wallet) { m_ctx->walletManager->walletOpened(wallet); } -void MainWindow::onWalletOpened() { +void MainWindow::onWalletOpened(Wallet *wallet) { qDebug() << Q_FUNC_INFO; if(m_wizard != nullptr) { m_wizard->hide(); @@ -710,59 +710,37 @@ void MainWindow::onConnectionStatusChanged(int status) } void MainWindow::onCreateTransactionSuccess(PendingTransaction *tx, const QVector &address) { - auto tx_status = tx->status(); - auto err = QString("Can't create transaction: "); - - if(tx_status != PendingTransaction::Status_Ok){ - auto tx_err = tx->errorString(); - qCritical() << tx_err; - - if (m_ctx->currentWallet->connectionStatus() == Wallet::ConnectionStatus_WrongVersion) - err = QString("%1 Wrong daemon version: %2").arg(err).arg(tx_err); - else - err = QString("%1 %2").arg(err).arg(tx_err); - - qDebug() << Q_FUNC_INFO << err; - QMessageBox::warning(this, "Transactions error", err); - m_ctx->currentWallet->disposeTransaction(tx); - } else if (tx->txCount() == 0) { - err = QString("%1 %2").arg(err).arg("No unmixable outputs to sweep."); - qDebug() << Q_FUNC_INFO << err; - QMessageBox::warning(this, "Transaction error", err); - m_ctx->currentWallet->disposeTransaction(tx); - } else { - const auto &description = m_ctx->tmpTxDescription; - - // Show advanced dialog on multi-destination transactions - if (address.size() > 1) { - auto *dialog_adv = new TxConfAdvDialog(m_ctx, description, this); - dialog_adv->setTransaction(tx); - dialog_adv->exec(); - dialog_adv->deleteLater(); - return; - } + const auto &description = m_ctx->tmpTxDescription; + + // Show advanced dialog on multi-destination transactions + if (address.size() > 1) { + auto *dialog_adv = new TxConfAdvDialog(m_ctx, description, this); + dialog_adv->setTransaction(tx); + dialog_adv->exec(); + dialog_adv->deleteLater(); + return; + } - auto *dialog = new TxConfDialog(m_ctx, tx, address[0], description, this); - switch (dialog->exec()) { - case QDialog::Rejected: - { - if (!dialog->showAdvanced) - m_ctx->onCancelTransaction(tx, address); - break; - } - case QDialog::Accepted: - m_ctx->currentWallet->commitTransactionAsync(tx); - break; + auto *dialog = new TxConfDialog(m_ctx, tx, address[0], description, this); + switch (dialog->exec()) { + case QDialog::Rejected: + { + if (!dialog->showAdvanced) + m_ctx->onCancelTransaction(tx, address); + break; } + case QDialog::Accepted: + m_ctx->currentWallet->commitTransactionAsync(tx); + break; + } - if (dialog->showAdvanced) { - auto *dialog_adv = new TxConfAdvDialog(m_ctx, description, this); - dialog_adv->setTransaction(tx); - dialog_adv->exec(); - dialog_adv->deleteLater(); - } - dialog->deleteLater(); + if (dialog->showAdvanced) { + auto *dialog_adv = new TxConfAdvDialog(m_ctx, description, this); + dialog_adv->setTransaction(tx); + dialog_adv->exec(); + dialog_adv->deleteLater(); } + dialog->deleteLater(); } void MainWindow::onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList& txid) { diff --git a/src/mainwindow.h b/src/mainwindow.h index 8e096ce..23c6239 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -138,7 +138,7 @@ public slots: // libwalletqt void onBalanceUpdated(quint64 balance, quint64 spendable); void onSynchronized(); - void onWalletOpened(); + void onWalletOpened(Wallet *wallet); void onWalletClosed(WalletWizard::Page page = WalletWizard::Page_Menu); void onConnectionStatusChanged(int status); void onCreateTransactionError(const QString &message); diff --git a/src/model/AddressBookModel.cpp b/src/model/AddressBookModel.cpp index a232da1..cea5cd1 100644 --- a/src/model/AddressBookModel.cpp +++ b/src/model/AddressBookModel.cpp @@ -154,6 +154,22 @@ int AddressBookModel::lookupPaymentID(const QString &payment_id) const return m_addressBook->lookupPaymentID(payment_id); } +QJsonArray AddressBookModel::toJsonArray(){ + QJsonArray arr; + for(int i = 0; i < this->rowCount(); i++) { + QJsonObject item; + QModelIndex index = this->index(i, 0); + const auto description = this->data(index.siblingAtColumn(AddressBookModel::Description), Qt::UserRole).toString().replace("\"", ""); + const auto address = this->data(index.siblingAtColumn(AddressBookModel::Address), Qt::UserRole).toString(); + if(address.isEmpty()) continue; + + item["description"] = description; + item["address"] = address; + arr << item; + } + return arr; +} + bool AddressBookModel::writeCSV(const QString &path) { QString csv = ""; for(int i = 0; i < this->rowCount(); i++) { diff --git a/src/model/AddressBookModel.h b/src/model/AddressBookModel.h index 9385428..0a4e2a2 100644 --- a/src/model/AddressBookModel.h +++ b/src/model/AddressBookModel.h @@ -30,6 +30,8 @@ public: Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QJsonArray toJsonArray(); + Q_INVOKABLE bool deleteRow(int row); Q_INVOKABLE int lookupPaymentID(const QString &payment_id) const; diff --git a/src/utils/FeatherSeed.h b/src/utils/FeatherSeed.h index c7a9346..b7dfbef 100644 --- a/src/utils/FeatherSeed.h +++ b/src/utils/FeatherSeed.h @@ -34,7 +34,7 @@ struct FeatherSeed { : lookup(lookup), coin(coin), language(language), mnemonic(mnemonic) { // Generate a new mnemonic if none was given - if (mnemonic.length() == 0) { + if (this->mnemonic.length() == 0) { this->time = std::time(nullptr); monero_seed seed(this->time, coin.toStdString()); @@ -49,10 +49,10 @@ struct FeatherSeed { this->setRestoreHeight(); } - if (mnemonic.length() == 25) { + if (this->mnemonic.length() == 25) { this->seedType = SeedType::MONERO; } - else if (mnemonic.length() == 14) { + else if (this->mnemonic.length() == 14) { this->seedType = SeedType::TEVADOR; } else { this->errorString = "Mnemonic seed does not match known type"; @@ -61,7 +61,7 @@ struct FeatherSeed { if (seedType == SeedType::TEVADOR) { try { - monero_seed seed(mnemonic.join(" ").toStdString(), coin.toStdString()); + monero_seed seed(this->mnemonic.join(" ").toStdString(), coin.toStdString()); this->time = seed.date(); this->setRestoreHeight(); diff --git a/src/utils/keysfiles.cpp b/src/utils/keysfiles.cpp index cc5bbfe..d61f417 100644 --- a/src/utils/keysfiles.cpp +++ b/src/utils/keysfiles.cpp @@ -47,6 +47,16 @@ int WalletKeysFiles::networkType() const { return m_networkType; } +QJsonObject WalletKeysFiles::toJsonObject() const { + auto item = QJsonObject(); + item["fileName"] = m_fileName; + item["modified"] = m_modified; + item["path"] = m_path; + item["networkType"] = m_networkType; + item["address"] = m_address; + return item; +} + WalletKeysFilesModel::WalletKeysFilesModel(AppContext *ctx, QObject *parent) : QAbstractTableModel(parent) , m_ctx(ctx) diff --git a/src/utils/keysfiles.h b/src/utils/keysfiles.h index 4c01edd..45112b5 100644 --- a/src/utils/keysfiles.h +++ b/src/utils/keysfiles.h @@ -19,6 +19,8 @@ public: int networkType() const; QString address() const; + QJsonObject toJsonObject() const; + private: QString m_fileName; qint64 m_modified; @@ -52,6 +54,10 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QStringList walletDirectories; + QList listWallets() { + return m_walletKeyFiles; + } + private: void updateDirectories(); diff --git a/src/utils/wsclient.cpp b/src/utils/wsclient.cpp index 478baa7..e816e94 100644 --- a/src/utils/wsclient.cpp +++ b/src/utils/wsclient.cpp @@ -77,7 +77,7 @@ void WSClient::onError(QAbstractSocket::SocketError error) { void WSClient::onbinaryMessageReceived(const QByteArray &message) { #ifdef QT_DEBUG - qDebug() << "WebSocket received:" << message; + qDebug() << "WebSocket (client) received:" << message; #endif if (!Utils::validateJSON(message)) { qCritical() << "Could not interpret WebSocket message as JSON"; diff --git a/src/utils/wsserver.cpp b/src/utils/wsserver.cpp new file mode 100644 index 0000000..afe1522 --- /dev/null +++ b/src/utils/wsserver.cpp @@ -0,0 +1,398 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "QtWebSockets/qwebsocketserver.h" +#include "QtWebSockets/qwebsocket.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "model/AddressBookModel.h" +#include "model/TransactionHistoryModel.h" +#include "libwalletqt/AddressBook.h" +#include "libwalletqt/TransactionHistory.h" +#include + +#include "wsserver.h" +#include "appcontext.h" +#include "utils/utils.h" + +WSServer::WSServer(AppContext *ctx, const QHostAddress &host, const quint16 port, const QString &password, bool debug, QObject *parent) : + QObject(parent), + m_debug(debug), + m_ctx(ctx), + m_password(password), + m_pWebSocketServer( + new QWebSocketServer(QStringLiteral("Feather Daemon WS"), + QWebSocketServer::NonSecureMode, this)) { + if (!m_pWebSocketServer->listen(QHostAddress::Any, port)) + return; + + qDebug() << "websocket server listening on port" << port; + + connect(m_pWebSocketServer, &QWebSocketServer::newConnection, this, &WSServer::onNewConnection); + connect(m_pWebSocketServer, &QWebSocketServer::closed, this, &WSServer::closed); + + connect(m_ctx, &AppContext::walletClosed, this, &WSServer::onWalletClosed); + connect(m_ctx, &AppContext::balanceUpdated, this, &WSServer::onBalanceUpdated); + connect(m_ctx, &AppContext::walletOpened, this, &WSServer::onWalletOpened); + connect(m_ctx, &AppContext::walletOpenedError, this, &WSServer::onWalletOpenedError); + connect(m_ctx, &AppContext::walletCreatedError, this, &WSServer::onWalletCreatedError); + connect(m_ctx, &AppContext::walletCreated, this, &WSServer::onWalletCreated); + connect(m_ctx, &AppContext::synchronized, this, &WSServer::onSynchronized); + connect(m_ctx, &AppContext::blockchainSync, this, &WSServer::onBlockchainSync); + connect(m_ctx, &AppContext::refreshSync, this, &WSServer::onRefreshSync); + connect(m_ctx, &AppContext::createTransactionError, this, &WSServer::onCreateTransactionError); + connect(m_ctx, &AppContext::createTransactionSuccess, this, &WSServer::onCreateTransactionSuccess); + connect(m_ctx, &AppContext::transactionCommitted, this, &WSServer::onTransactionCommitted); + connect(m_ctx, &AppContext::walletOpenPasswordNeeded, this, &WSServer::onWalletOpenPasswordRequired); + connect(m_ctx, &AppContext::initiateTransaction, this, &WSServer::onInitiateTransaction); + + m_walletDir = m_ctx->defaultWalletDir; + + // Bootstrap Tor/websockets + m_ctx->initTor(); + m_ctx->initWS(); +} + +QString WSServer::connectionId(QWebSocket *pSocket) { + return QString("%1#%2").arg(pSocket->peerAddress().toString()).arg(pSocket->peerPort()); +} + +void WSServer::onNewConnection() { + QWebSocket *pSocket = m_pWebSocketServer->nextPendingConnection(); + + connect(pSocket, &QWebSocket::binaryMessageReceived, this, &WSServer::processBinaryMessage); + connect(pSocket, &QWebSocket::disconnected, this, &WSServer::socketDisconnected); + + m_clients << pSocket; + m_clients_auth[this->connectionId(pSocket)] = false; + + // blast wallet listing on connect + QJsonArray arr; + for(const WalletKeysFiles &wallet: m_ctx->listWallets()) + arr << wallet.toJsonObject(); + auto welcomeWalletMessage = WSServer::createWSMessage("walletList", arr); + pSocket->sendBinaryMessage(welcomeWalletMessage); + + // and the current state of appcontext + QJsonObject obj; + + if(this->m_ctx->currentWallet == nullptr) { + obj["state"] = "walletClosed"; + } + else { + obj["state"] = "walletOpened"; + obj["walletPath"] = m_ctx->currentWallet->path(); + } + this->sendAll("state", obj); +} + +void WSServer::processBinaryMessage(QByteArray buffer) { + QWebSocket *pClient = qobject_cast(sender()); + const QString cid = this->connectionId(pClient); + + if (m_debug) + qDebug() << "Websocket (server) received:" << buffer; + if (!pClient) + return; + + QJsonDocument doc = QJsonDocument::fromJson(buffer); + QJsonObject object = doc.object(); + + QString cmd = object.value("cmd").toString(); + + if(m_clients_auth.contains(cid) && !m_clients_auth[cid]) { + if (cmd == "password") { + auto data = object.value("data").toObject(); + auto passwd = data.value("password").toString(); + if(passwd != this->m_password) { + this->sendAll("passwordIncorrect", "authentication failed."); + return; + } else { + this->m_clients_auth[cid] = true; + this->sendAll("passwordSuccess", "authentication OK."); + return; + } + } else { + this->sendAll("passwordIncorrect", "authentication failed."); + return; + } + } + + if(cmd == "openWallet") { + auto data = object.value("data").toObject(); + auto path = data.value("path").toString(); + auto passwd = data.value("password").toString(); + + m_ctx->onOpenWallet(path, passwd); + } else if (cmd == "closeWallet") { + if (m_ctx->currentWallet == nullptr) + return; + + m_ctx->closeWallet(true, true); + } else if(cmd == "addressList") { + auto data = object.value("data").toObject(); + auto accountIndex = data.value("accountIndex").toInt(); + auto addressIndex = data.value("addressIndex").toInt(); + + auto limit = data.value("limit").toInt(50); + auto offset = data.value("offset").toInt(0); + + QJsonArray arr; + for(int i = offset; i != limit; i++) { + arr << m_ctx->currentWallet->address((quint32) accountIndex, (quint32) addressIndex + i); + } + + QJsonObject obj; + obj["accountIndex"] = accountIndex; + obj["addressIndex"] = addressIndex; + obj["offset"] = offset; + obj["limit"] = limit; + obj["addresses"] = arr; + this->sendAll("addressList", arr); + } else if(cmd == "sendTransaction") { + auto data = object.value("data").toObject(); + auto address = data.value("address").toString(); + auto amount = data.value("amount").toDouble(0); + auto description = data.value("description").toString(); + bool all = data.value("all").toBool(false); + + if(!WalletManager::addressValid(address, m_ctx->currentWallet->nettype())){ + this->sendAll("transactionError", "Could not validate address"); + return; + } + + if(amount <= 0) { + this->sendAll("transactionError", "y u send 0"); + return; + } + + m_ctx->onCreateTransaction(address, (quint64) amount, description, all); + } else if(cmd == "createWallet") { + auto data = object.value("data").toObject(); + + auto name = data.value("name").toString(); + auto path = data.value("path").toString(); + auto password = data.value("password").toString(); + QString walletPath; + + if(name.isEmpty()){ + this->sendAll("walletCreatedError", "Supply a name for your wallet"); + return; + } + + if(path.isEmpty()) { + walletPath = QDir(m_walletDir).filePath(name + ".keys"); + if(Utils::fileExists(walletPath)) { + auto err = QString("Filepath already exists: %1").arg(walletPath); + this->sendAll("walletCreatedError", err); + return; + } + } + + FeatherSeed seed = FeatherSeed(m_ctx->restoreHeights[m_ctx->networkType], m_ctx->coinName, m_ctx->seedLanguage); + m_ctx->createWallet(seed, walletPath, password); + } else if(cmd == "transactionHistory") { + m_ctx->currentWallet->history()->refresh(m_ctx->currentWallet->currentSubaddressAccount()); + auto *model = m_ctx->currentWallet->history(); + + QJsonArray arr = model->toJsonArray(); + this->sendAll("transactionHistory", arr); + } else if (cmd == "addressBook") { + QJsonArray arr = m_ctx->currentWallet->addressBookModel()->toJsonArray(); + this->sendAll("addressBook", arr); + } +} + +void WSServer::socketDisconnected() { + QWebSocket *pClient = qobject_cast(sender()); + QString cid = connectionId(pClient); + + m_clients_auth[cid] = false; + + if (m_debug) + qDebug() << "socketDisconnected:" << pClient; + if (pClient) { + m_clients.removeAll(pClient); + pClient->deleteLater(); + } +} + +// templates are forbidden! +QByteArray WSServer::createWSMessage(const QString &cmd, const QJsonArray &arr) { + QJsonObject jsonObject = QJsonObject(); + jsonObject["cmd"] = cmd; + jsonObject["data"] = arr; + QJsonDocument doc = QJsonDocument(jsonObject); + return doc.toJson(QJsonDocument::Compact); +} +QByteArray WSServer::createWSMessage(const QString &cmd, const QJsonObject &obj) { + QJsonObject jsonObject = QJsonObject(); + jsonObject["cmd"] = cmd; + jsonObject["data"] = obj; + QJsonDocument doc = QJsonDocument(jsonObject); + return doc.toJson(QJsonDocument::Compact); +} + +QByteArray WSServer::createWSMessage(const QString &cmd, const int val) { + QJsonObject jsonObject = QJsonObject(); + jsonObject["cmd"] = cmd; + jsonObject["data"] = val; + QJsonDocument doc = QJsonDocument(jsonObject); + return doc.toJson(QJsonDocument::Compact); +} + +QByteArray WSServer::createWSMessage(const QString &cmd, const QString &val) { + QJsonObject jsonObject = QJsonObject(); + jsonObject["cmd"] = cmd; + jsonObject["data"] = val; + QJsonDocument doc = QJsonDocument(jsonObject); + return doc.toJson(QJsonDocument::Compact); +} + +WSServer::~WSServer() { + m_pWebSocketServer->close(); + qDeleteAll(m_clients.begin(), m_clients.end()); +} + +void WSServer::sendAll(const QString &cmd, const QJsonObject &obj) { + for(QWebSocket *pSocket: m_clients) { + pSocket->sendBinaryMessage(WSServer::createWSMessage(cmd, obj)); + } +} + +void WSServer::sendAll(const QString &cmd, const QJsonArray &arr) { + for(QWebSocket *pSocket: m_clients) { + pSocket->sendBinaryMessage(WSServer::createWSMessage(cmd, arr)); + } +} + +void WSServer::sendAll(const QString &cmd, int val) { + for(QWebSocket *pSocket: m_clients) { + pSocket->sendBinaryMessage(WSServer::createWSMessage(cmd, val)); + } +} + +void WSServer::sendAll(const QString &cmd, const QString &val) { + for(QWebSocket *pSocket: m_clients) { + pSocket->sendBinaryMessage(WSServer::createWSMessage(cmd, val)); + } +} + +// ====================================================================== + +void WSServer::onWalletOpened(Wallet *wallet) { + connect(m_ctx->currentWallet, &Wallet::connectionStatusChanged, this, &WSServer::onConnectionStatusChanged); + + auto obj = wallet->toJsonObject(); + sendAll("walletOpened", obj); +} + +void WSServer::onBlockchainSync(int height, int target) { + QJsonObject obj; + obj["height"] = height; + obj["target"] = target; + sendAll("blockchainSync", obj); +} + +void WSServer::onRefreshSync(int height, int target) { + QJsonObject obj; + obj["height"] = height; + obj["target"] = target; + sendAll("refreshSync", obj); +} + +void WSServer::onWalletClosed() { + QJsonObject obj; + sendAll("walletClosed", obj); +} + +void WSServer::onBalanceUpdated(quint64 balance, quint64 spendable) { + QJsonObject obj; + obj["balance"] = balance / globals::cdiv; + obj["spendable"] = spendable / globals::cdiv; + sendAll("balanceUpdated", obj); +} + +void WSServer::onWalletOpenedError(const QString &err) { + sendAll("walletOpenedError", err); +} + +void WSServer::onWalletCreatedError(const QString &err) { + sendAll("walletCreatedError", err); +} + +void WSServer::onWalletCreated(Wallet *wallet) { + auto obj = wallet->toJsonObject(); + sendAll("walletCreated", obj); + + // emit signal on behalf of walletManager + m_ctx->walletManager->walletOpened(wallet); +} + +void WSServer::onSynchronized() { + QJsonObject obj; + sendAll("synchronized", obj); +} + +void WSServer::onWalletOpenPasswordRequired(bool invalidPassword, const QString &path) { + QJsonObject obj; + obj["invalidPassword"] = invalidPassword; + obj["path"] = path; + sendAll("synchronized", obj); +} + +void WSServer::onConnectionStatusChanged(int status) { + sendAll("connectionStatusChanged", status); +} + +void WSServer::onInitiateTransaction() { + QJsonObject obj; + sendAll("transactionStarted", obj); +} + +void WSServer::onCreateTransactionError(const QString &message) { + sendAll("transactionError", message); +} + +void WSServer::onCreateTransactionSuccess(PendingTransaction *tx, const QVector &address) { + // auto-commit all tx's + m_ctx->currentWallet->commitTransactionAsync(tx); +} + +void WSServer::onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList &txid) { + QString preferredCur = config()->get(Config::preferredFiatCurrency).toString(); + + auto convert = [preferredCur](double amount){ + return QString::number(AppContext::prices->convert("WOW", preferredCur, amount), 'f', 2); + }; + + QJsonObject obj; + QJsonArray txids; + + for(const QString &id: txid) + txids << id; + + obj["txid"] = txids; + obj["status"] = status; + obj["amount"] = tx->amount() / globals::cdiv; + obj["fee"] = tx->fee() / globals::cdiv; + obj["total"] = (tx->amount() + tx->fee()) / globals::cdiv; + + obj["amount_fiat"] = convert(tx->amount() / globals::cdiv); + obj["fee_fiat"] = convert(tx->fee() / globals::cdiv); + obj["total_fiat"] = convert((tx->amount() + tx->fee()) / globals::cdiv); + + sendAll("transactionSent", obj); +} \ No newline at end of file diff --git a/src/utils/wsserver.h b/src/utils/wsserver.h new file mode 100644 index 0000000..fd5beb4 --- /dev/null +++ b/src/utils/wsserver.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_WSSERVER_H +#define FEATHER_WSSERVER_H + + +#include +#include +#include + +#include "appcontext.h" +#include "utils/keysfiles.h" +#include "qrcode/QrCode.h" + +#include "libwalletqt/WalletManager.h" + +QT_FORWARD_DECLARE_CLASS(QWebSocketServer) +QT_FORWARD_DECLARE_CLASS(QWebSocket) + +class WSServer : public QObject +{ +Q_OBJECT +public: + explicit WSServer(AppContext *ctx, const QHostAddress &host, const quint16 port, const QString &password, bool debug = false, QObject *parent = nullptr); + ~WSServer(); + +signals: + void closed(); + +private slots: + void onNewConnection(); + void processBinaryMessage(QByteArray buffer); + void socketDisconnected(); + + // libwalletqt + void onBalanceUpdated(quint64 balance, quint64 spendable); + void onSynchronized(); + void onWalletOpened(Wallet *wallet); + void onWalletClosed(); + void onConnectionStatusChanged(int status); + void onCreateTransactionError(const QString &message); + void onCreateTransactionSuccess(PendingTransaction *tx, const QVector &address); + void onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList& txid); + void onBlockchainSync(int height, int target); + void onRefreshSync(int height, int target); + void onWalletOpenedError(const QString &err); + void onWalletCreatedError(const QString &err); + void onWalletCreated(Wallet *wallet); + void onWalletOpenPasswordRequired(bool invalidPassword, const QString &path); + void onInitiateTransaction(); + +private: + QWebSocketServer *m_pWebSocketServer; + QList m_clients; + QMap m_clients_auth; + bool m_debug; + QString m_walletDir; + AppContext *m_ctx; + QString m_password; + + QString connectionId(QWebSocket *pSocket); + + QByteArray createWSMessage(const QString &cmd, const QJsonObject &obj); + QByteArray createWSMessage(const QString &cmd, const QJsonArray &arr); + QByteArray createWSMessage(const QString &cmd, const int val); + QByteArray createWSMessage(const QString &cmd, const QString &val); + void sendAll(const QString &cmd, const QJsonArray &arr); + void sendAll(const QString &cmd, const QJsonObject &obj); + void sendAll(const QString &cmd, int val); + void sendAll(const QString &cmd, const QString &val); +}; + +#endif //FEATHER_WSSERVER_H diff --git a/src/widgets/xmrigwidget.cpp b/src/widgets/xmrigwidget.cpp index e1b131c..3574fbe 100644 --- a/src/widgets/xmrigwidget.cpp +++ b/src/widgets/xmrigwidget.cpp @@ -102,7 +102,7 @@ void XMRigWidget::onWalletClosed() { ui->lineEdit_address->setText(""); } -void XMRigWidget::onWalletOpened(){ +void XMRigWidget::onWalletOpened(Wallet *wallet){ // Xmrig username auto username = m_ctx->currentWallet->getCacheAttribute("feather.xmrig_username"); if(!username.isEmpty()) diff --git a/src/widgets/xmrigwidget.h b/src/widgets/xmrigwidget.h index 59eeeaf..d146204 100644 --- a/src/widgets/xmrigwidget.h +++ b/src/widgets/xmrigwidget.h @@ -27,7 +27,7 @@ public: public slots: void onWalletClosed(); - void onWalletOpened(); + void onWalletOpened(Wallet *wallet); void onStartClicked(); void onStopClicked(); void onClearClicked(); diff --git a/src/wizard/menu.ui b/src/wizard/menu.ui index e2cce3d..493df71 100644 --- a/src/wizard/menu.ui +++ b/src/wizard/menu.ui @@ -80,16 +80,6 @@ - - - - false - - - by dsc & tobtoht - - -