This commit introduces a websocket server via the --daemon argument.

```
./wowlet --daemon 127.0.0.1:1234 --daemon-password "sekrit"
```

The wallet will start in the background and expose a websocket port that you can connect to using a websocket client. This way, you will be able to control the wallet via websockets. The commands are defined in wsserver.cpp, in the `processBinaryMessage()` function.

- `openWallet` - opens a wallet by path/password.
- `closeWallet` - close current wallet.
- `addressList` - Returns a list of receive addresses.
- `sendTransaction` - Creates and sends a transaction.
- `createWallet` - Create a wallet by path/password.
- `transactionHistory` - Returns the complete list of transactions
- `addressBook` - Returns the complete list of address book entries.

Messages sent back and forth between the server and client are JSON. There is a Python example client available over at https://git.wownero.com/wownero/wowlet-ws-client
This commit is contained in:
dsc 2021-03-27 19:59:21 +01:00
parent 40a575a5d6
commit 8968a8cbce
24 changed files with 732 additions and 80 deletions

View file

@ -23,6 +23,7 @@ QMap<QString, QString> AppContext::txDescriptionCache;
QMap<QString, QString> 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 QVector<QStr
// Let UI know that the transaction was constructed
emit endTransaction();
// Some validation
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 (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);
}

View file

@ -89,6 +89,12 @@ public:
static QMap<QString, QString> txCache;
static TxFiatHistory *txFiatHistory;
QList<WalletKeysFiles> 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<QString> &addresses, const QVector<quint64> &amounts, const QString &description);
void onCancelTransaction(PendingTransaction *tx, const QVector<QString> &address);
void onSweepOutput(const QString &keyImage, QString address, bool churn, int outputs) const;
@ -150,7 +156,7 @@ signals:
void synchronized();
void blockHeightWSUpdated(QMap<QString, int> 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"));

View file

@ -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);
}
}

View file

@ -6,10 +6,12 @@
#include <QtCore>
#include "appcontext.h"
#include <utils/wsserver.h>
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);

View file

@ -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<QString, size_t>::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);

View file

@ -24,6 +24,7 @@ public:
Q_INVOKABLE bool getRow(int index, std::function<void (AddressBookInfo &)> 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;

View file

@ -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;
}

View file

@ -32,6 +32,7 @@ public:
Q_INVOKABLE void refresh(quint32 accountIndex);
Q_INVOKABLE void setTxNote(const QString &txid, const QString &note);
Q_INVOKABLE bool writeCSV(const QString &path);
Q_INVOKABLE QJsonArray toJsonArray();
quint64 count() const;
QDateTime firstDateTime() const;
QDateTime lastDateTime() const;

View file

@ -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)

View file

@ -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

View file

@ -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<QVector<QString>>();
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<QVector<QString>>();
auto *mainWindow = new MainWindow(ctx);
return QApplication::exec();

View file

@ -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<const QString &, quint64, const QString &, bool>::of(&AppContext::onCreateTransaction));
connect(ui->sendWidget, &SendWidget::createTransaction, m_ctx, QOverload<const QString, quint64, const QString, bool>::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<QString> &address) {
auto tx_status = tx->status();
auto err = QString("Can't create transaction: ");
const auto &description = m_ctx->tmpTxDescription;
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;
}
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();
// 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;
}
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) {

View file

@ -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);

View file

@ -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++) {

View file

@ -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;

View file

@ -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();

View file

@ -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)

View file

@ -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<WalletKeysFiles> listWallets() {
return m_walletKeyFiles;
}
private:
void updateDirectories();

View file

@ -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";

398
src/utils/wsserver.cpp Normal file
View file

@ -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 <QtCore/QDebug>
#include <QtNetwork>
#include <QScreen>
#include <QMessageBox>
#include <QtNetwork>
#include <QClipboard>
#include <QCompleter>
#include <QDesktopServices>
#include <QTextCodec>
#include <model/SubaddressModel.h>
#include <model/SubaddressProxyModel.h>
#include <model/CoinsModel.h>
#include <model/CoinsProxyModel.h>
#include "model/AddressBookModel.h"
#include "model/TransactionHistoryModel.h"
#include "libwalletqt/AddressBook.h"
#include "libwalletqt/TransactionHistory.h"
#include <globals.h>
#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<QWebSocket *>(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<QWebSocket *>(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<QString> &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);
}

74
src/utils/wsserver.h Normal file
View file

@ -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 <QObject>
#include <QList>
#include <QtNetwork>
#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<QString> &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<QWebSocket *> m_clients;
QMap<QString, bool> 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

View file

@ -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())

View file

@ -27,7 +27,7 @@ public:
public slots:
void onWalletClosed();
void onWalletOpened();
void onWalletOpened(Wallet *wallet);
void onStartClicked();
void onStopClicked();
void onClearClicked();

View file

@ -80,16 +80,6 @@
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>by dsc &amp; tobtoht</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>