feather/src/appcontext.cpp
2021-05-22 15:42:46 +02:00

406 lines
14 KiB
C++

// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#include <QDir>
#include <QMessageBox>
#include "appcontext.h"
#include "constants.h"
// libwalletqt
#include "libwalletqt/TransactionHistory.h"
#include "libwalletqt/Subaddress.h"
#include "libwalletqt/Coins.h"
#include "model/TransactionHistoryModel.h"
#include "model/SubaddressModel.h"
#include "utils/NetworkManager.h"
#include "utils/WebsocketClient.h"
#include "utils/WebsocketNotifier.h"
// This class serves as a business logic layer between MainWindow and libwalletqt.
// This way we don't clutter the GUI with wallet logic,
// and keep libwalletqt (mostly) clean of Feather specific implementation details
AppContext::AppContext(Wallet *wallet)
: wallet(wallet)
, nodes(new Nodes(this, this))
, networkType(constants::networkType)
, m_rpc(new DaemonRpc{this, getNetworkTor(), ""})
{
connect(this->wallet.get(), &Wallet::moneySpent, this, &AppContext::onMoneySpent);
connect(this->wallet.get(), &Wallet::moneyReceived, this, &AppContext::onMoneyReceived);
connect(this->wallet.get(), &Wallet::unconfirmedMoneyReceived, this, &AppContext::onUnconfirmedMoneyReceived);
connect(this->wallet.get(), &Wallet::newBlock, this, &AppContext::onWalletNewBlock);
connect(this->wallet.get(), &Wallet::updated, this, &AppContext::onWalletUpdate);
connect(this->wallet.get(), &Wallet::refreshed, this, &AppContext::onWalletRefreshed);
connect(this->wallet.get(), &Wallet::transactionCommitted, this, &AppContext::onTransactionCommitted);
connect(this->wallet.get(), &Wallet::heightRefreshed, this, &AppContext::onHeightRefreshed);
connect(this->wallet.get(), &Wallet::transactionCreated, this, &AppContext::onTransactionCreated);
connect(this->wallet.get(), &Wallet::deviceError, this, &AppContext::onDeviceError);
connect(this->wallet.get(), &Wallet::deviceButtonRequest, this, &AppContext::onDeviceButtonRequest);
connect(this->wallet.get(), &Wallet::connectionStatusChanged, [this]{
this->nodes->autoConnect();
});
connect(this->wallet.get(), &Wallet::currentSubaddressAccountChanged, [this]{
this->updateBalance();
});
connect(this, &AppContext::createTransactionError, this, &AppContext::onCreateTransactionError);
// Store the wallet every 2 minutes
m_storeTimer.start(2 * 60 * 1000);
connect(&m_storeTimer, &QTimer::timeout, [this](){
this->storeWallet();
});
this->updateBalance();
// force trigger preferredFiat signal for history model
this->onPreferredFiatCurrencyChanged(config()->get(Config::preferredFiatCurrency).toString());
connect(this->wallet->history(), &TransactionHistory::txNoteChanged, [this]{
this->wallet->history()->refresh(this->wallet->currentSubaddressAccount());
});
}
// ################## Transaction creation ##################
void AppContext::onCreateTransaction(const QString &address, quint64 amount, const QString &description, bool all) {
this->tmpTxDescription = description;
if (!all && amount == 0) {
emit createTransactionError("Cannot send nothing");
return;
}
quint64 unlocked_balance = this->wallet->unlockedBalance();
if (!all && amount > unlocked_balance) {
emit createTransactionError("Not enough money to spend");
return;
} else if (unlocked_balance == 0) {
emit createTransactionError("No money to spend");
return;
}
qInfo() << "Creating transaction";
if (all)
this->wallet->createTransactionAllAsync(address, "", constants::mixin, this->tx_priority);
else
this->wallet->createTransactionAsync(address, "", amount, constants::mixin, this->tx_priority);
emit initiateTransaction();
}
void AppContext::onCreateTransactionMultiDest(const QVector<QString> &addresses, const QVector<quint64> &amounts, const QString &description) {
this->tmpTxDescription = description;
quint64 total_amount = 0;
for (auto &amount : amounts) {
total_amount += amount;
}
auto unlocked_balance = this->wallet->unlockedBalance();
if (total_amount > unlocked_balance) {
emit createTransactionError("Not enough money to spend");
}
qInfo() << "Creating transaction";
this->wallet->createTransactionMultiDestAsync(addresses, amounts, this->tx_priority);
emit initiateTransaction();
}
void AppContext::onSweepOutput(const QString &keyImage, QString address, bool churn, int outputs) {
if (churn) {
address = this->wallet->address(0, 0); // primary address
}
qInfo() << "Creating transaction";
this->wallet->createTransactionSingleAsync(keyImage, address, outputs, this->tx_priority);
emit initiateTransaction();
}
void AppContext::onCreateTransactionError(const QString &msg) {
this->tmpTxDescription = "";
emit endTransaction();
}
void AppContext::onCancelTransaction(PendingTransaction *tx, const QVector<QString> &address) {
// tx cancelled by user
emit createTransactionCancelled(address, tx->amount());
this->wallet->disposeTransaction(tx);
}
void AppContext::commitTransaction(PendingTransaction *tx) {
// Nodes - even well-connected, properly configured ones - consistently fail to relay transactions
// To mitigate transactions failing we just send the transaction to every node we know about over Tor
if (config()->get(Config::multiBroadcast).toBool()) {
this->onMultiBroadcast(tx);
}
this->wallet->commitTransactionAsync(tx);
}
void AppContext::onMultiBroadcast(PendingTransaction *tx) {
quint64 count = tx->txCount();
for (quint64 i = 0; i < count; i++) {
QString txData = tx->signedTxToHex(i);
for (const auto& node: this->nodes->websocketNodes()) {
if (!node.online) continue;
QString address = node.toURL();
qDebug() << QString("Relaying %1 to: %2").arg(tx->txid()[i], address);
m_rpc->setDaemonAddress(address);
m_rpc->sendRawTransaction(txData);
}
}
}
// ################## Models ##################
void AppContext::onPreferredFiatCurrencyChanged(const QString &symbol) {
auto *model = this->wallet->transactionHistoryModel();
if (model != nullptr) {
model->preferredFiatSymbol = symbol;
}
}
void AppContext::onAmountPrecisionChanged(int precision) {
auto *model = this->wallet->transactionHistoryModel();
if (!model) return;
model->amountPrecision = precision;
}
// ################## Device ##################
void AppContext::onDeviceButtonRequest(quint64 code) {
emit deviceButtonRequest(code);
}
void AppContext::onDeviceError(const QString &message) {
qCritical() << "Device error: " << message;
emit deviceError(message);
}
// ################## Misc ##################
void AppContext::onTorSettingsChanged() {
if (Utils::isTorsocks()) {
return;
}
this->nodes->connectToNode();
auto privacyLevel = config()->get(Config::torPrivacyLevel).toInt();
qDebug() << "Changed privacyLevel to " << privacyLevel;
}
void AppContext::onSetRestoreHeight(quint64 height){
auto seed = this->wallet->getCacheAttribute("feather.seed");
if(!seed.isEmpty()) {
const auto msg = "This wallet has a 14 word mnemonic seed which has the restore height embedded.";
emit setRestoreHeightError(msg);
return;
}
this->wallet->setWalletCreationHeight(height);
this->wallet->setPassword(this->wallet->getPassword()); // trigger .keys write
// nuke wallet cache
const auto fn = this->wallet->cachePath();
WalletManager::clearWalletCache(fn);
emit customRestoreHeightSet(height);
}
void AppContext::onOpenAliasResolve(const QString &openAlias) {
// @TODO: calling this freezes for about 1-2 seconds :/
const auto result = WalletManager::instance()->resolveOpenAlias(openAlias);; // TODO: async call
const auto spl = result.split("|");
auto msg = QString("");
if(spl.count() != 2) {
msg = "Internal error";
emit openAliasResolveError(msg);
return;
}
const auto &status = spl.at(0);
const auto &address = spl.at(1);
const auto valid = WalletManager::addressValid(address, constants::networkType);
if(status == "false"){
if(valid){
msg = "Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed";
emit openAliasResolveError(msg);
return;
} else {
msg = "No valid address found at this OpenAlias address, but the DNSSEC signatures could not be verified, so this may be spoofed";
emit openAliasResolveError(msg);
return;
}
} else if(status != "true") {
msg = "Internal error";
emit openAliasResolveError(msg);
return;
}
if(valid){
emit openAliasResolved(address, openAlias);
return;
}
msg = QString("Address validation error.");
if(!address.isEmpty())
msg += QString(" Perhaps it is of the wrong network type."
"\n\nOpenAlias: %1\nAddress: %2").arg(openAlias).arg(address);
emit openAliasResolveError(msg);
}
// ########################################## LIBWALLET QT SIGNALS ####################################################
void AppContext::onMoneySpent(const QString &txId, quint64 amount) {
// Outgoing tx included in a block
qDebug() << Q_FUNC_INFO << txId << " " << WalletManager::displayAmount(amount);
}
void AppContext::onMoneyReceived(const QString &txId, quint64 amount) {
// Incoming tx included in a block.
qDebug() << Q_FUNC_INFO << txId << " " << WalletManager::displayAmount(amount);
}
void AppContext::onUnconfirmedMoneyReceived(const QString &txId, quint64 amount) {
// Incoming tx in pool
qDebug() << Q_FUNC_INFO << txId << " " << WalletManager::displayAmount(amount);
if (this->wallet->synchronized()) {
auto notify = QString("%1 XMR (pending)").arg(WalletManager::displayAmount(amount, false));
Utils::desktopNotify("Payment received", notify, 5000);
}
}
void AppContext::onWalletUpdate() {
if (this->wallet->synchronized()) {
this->refreshModels();
this->storeWallet();
}
this->updateBalance();
}
void AppContext::onWalletRefreshed(bool success, const QString &message) {
if (!success) {
// Something went wrong during refresh, in some cases we need to notify the user
qCritical() << "Exception during refresh: " << message; // Can't use ->errorString() here, other SLOT might snipe it first
return;
}
if (!this->refreshed) {
refreshModels();
this->refreshed = true;
emit walletRefreshed();
// store wallet immediately upon finishing synchronization
this->wallet->store();
}
qDebug() << "Wallet refresh status: " << success;
}
void AppContext::onWalletNewBlock(quint64 blockheight, quint64 targetHeight) {
// Called whenever a new block gets scanned by the wallet
this->syncStatusUpdated(blockheight, targetHeight);
if (this->wallet->isSynchronized()) {
this->wallet->coins()->refreshUnlocked();
this->wallet->history()->refresh(this->wallet->currentSubaddressAccount());
// Todo: only refresh tx confirmations
}
}
void AppContext::onHeightRefreshed(quint64 walletHeight, quint64 daemonHeight, quint64 targetHeight) {
qDebug() << Q_FUNC_INFO << walletHeight << daemonHeight << targetHeight;
if (this->wallet->connectionStatus() == Wallet::ConnectionStatus_Disconnected)
return;
if (daemonHeight < targetHeight) {
emit blockchainSync(daemonHeight, targetHeight);
}
else {
this->syncStatusUpdated(walletHeight, daemonHeight);
}
}
void AppContext::onTransactionCreated(PendingTransaction *tx, const QVector<QString> &address) {
qDebug() << Q_FUNC_INFO;
for (auto &addr : address) {
if (addr == constants::donationAddress) {
this->donationSending = true;
}
}
// Let UI know that the transaction was constructed
emit endTransaction();
// tx created, but not sent yet. ask user to verify first.
emit createTransactionSuccess(tx, address);
}
void AppContext::onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList& txid){
if (status) {
for (const auto &entry: txid) {
this->wallet->setUserNote(entry, this->tmpTxDescription);
}
this->tmpTxDescription = "";
}
// Store wallet immediately so we don't risk losing tx key if wallet crashes
this->wallet->store();
this->wallet->history()->refresh(this->wallet->currentSubaddressAccount());
this->wallet->coins()->refresh(this->wallet->currentSubaddressAccount());
this->updateBalance();
// this tx was a donation to Feather, stop our nagging
if (this->donationSending) {
this->donationSending = false;
config()->set(Config::donateBeg, -1);
}
emit transactionCommitted(status, tx, txid);
}
void AppContext::storeWallet() {
// Do not store a synchronizing wallet: store() is NOT thread safe and may crash the wallet
if (!this->wallet->isSynchronized())
return;
qDebug() << "Storing wallet";
this->wallet->store();
}
void AppContext::updateBalance() {
quint64 balance = this->wallet->balance();
quint64 spendable = this->wallet->unlockedBalance();
emit balanceUpdated(balance, spendable);
}
void AppContext::syncStatusUpdated(quint64 height, quint64 target) {
if (height < (target - 1)) {
emit refreshSync(height, target);
}
else {
this->updateBalance();
emit synchronized();
}
}
void AppContext::refreshModels() {
this->wallet->history()->refresh(this->wallet->currentSubaddressAccount());
this->wallet->subaddress()->refresh(this->wallet->currentSubaddressAccount());
this->wallet->coins()->refresh(this->wallet->currentSubaddressAccount());
// Todo: set timer for refreshes
}