2020-10-07 10:36:04 +00:00
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Copyright (c) 2020, The Monero Project.
|
|
|
|
|
|
|
|
#include <stdexcept>
|
|
|
|
#include <QDir>
|
|
|
|
#include <QStandardPaths>
|
|
|
|
#include <QMessageBox>
|
|
|
|
#include <QClipboard>
|
|
|
|
#include <QDesktopWidget>
|
|
|
|
|
|
|
|
#include "appcontext.h"
|
|
|
|
#include "utils/tails.h"
|
|
|
|
#include "utils/whonix.h"
|
|
|
|
#include "utils/utils.h"
|
|
|
|
#include "utils/prices.h"
|
|
|
|
#include "utils/networktype.h"
|
|
|
|
#include "utils/wsclient.h"
|
|
|
|
|
|
|
|
// libwalletqt
|
|
|
|
#include "libwalletqt/WalletManager.h"
|
|
|
|
#include "libwalletqt/Wallet.h"
|
|
|
|
#include "libwalletqt/TransactionHistory.h"
|
|
|
|
#include "libwalletqt/SubaddressAccount.h"
|
|
|
|
#include "libwalletqt/Subaddress.h"
|
|
|
|
#include "libwalletqt/Coins.h"
|
|
|
|
#include "model/TransactionHistoryModel.h"
|
|
|
|
#include "model/SubaddressAccountModel.h"
|
|
|
|
#include "model/SubaddressModel.h"
|
|
|
|
#include "utils/keysfiles.h"
|
|
|
|
#include "utils/networktype.h"
|
|
|
|
#include "utils/config.h"
|
|
|
|
|
|
|
|
|
|
|
|
Prices *AppContext::prices = nullptr;
|
|
|
|
WalletKeysFilesModel *AppContext::wallets = nullptr;
|
|
|
|
TxFiatHistory *AppContext::txFiatHistory = nullptr;
|
|
|
|
double AppContext::balance = 0;
|
|
|
|
QMap<QString, QString> AppContext::txDescriptionCache;
|
|
|
|
|
|
|
|
AppContext::AppContext(QCommandLineParser *cmdargs) {
|
|
|
|
this->network = new QNetworkAccessManager();
|
|
|
|
this->networkClearnet = new QNetworkAccessManager();
|
|
|
|
this->cmdargs = cmdargs;
|
|
|
|
|
|
|
|
#if defined(Q_OS_MAC)
|
|
|
|
this->isTorSocks = qgetenv("DYLD_INSERT_LIBRARIES").indexOf("libtorsocks") >= 0;
|
|
|
|
#elif defined(Q_OS_LINUX)
|
|
|
|
this->isTorSocks = qgetenv("LD_PRELOAD").indexOf("libtorsocks") >= 0;
|
|
|
|
#elif defined(Q_OS_WIN)
|
|
|
|
this->isTorSocks = false;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
this->isTails = TailsOS::detect();
|
|
|
|
this->isWhonix = WhonixOS::detect();
|
|
|
|
|
|
|
|
//Paths
|
|
|
|
this->configRoot = QDir::homePath();
|
|
|
|
if (isTails) { // #if defined(PORTABLE)
|
|
|
|
QString portablePath = []{
|
|
|
|
QString appImagePath = qgetenv("APPIMAGE");
|
|
|
|
if (appImagePath.isEmpty()) {
|
|
|
|
qDebug() << "Not an appimage, using currentPath()";
|
|
|
|
return QDir::currentPath() + "/.feather";
|
|
|
|
}
|
|
|
|
|
|
|
|
QFileInfo appImageDir(appImagePath);
|
|
|
|
return appImageDir.absoluteDir().path() + "/.feather";
|
|
|
|
}();
|
|
|
|
|
|
|
|
|
|
|
|
if (QDir().mkpath(portablePath)) {
|
|
|
|
this->configRoot = portablePath;
|
|
|
|
} else {
|
|
|
|
qCritical() << "Unable to create portable directory: " << portablePath;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this->accountName = Utils::getUnixAccountName();
|
|
|
|
this->homeDir = QDir::homePath();
|
|
|
|
|
|
|
|
#if defined(Q_OS_LINUX) or defined(Q_OS_MAC)
|
|
|
|
this->defaultWalletDir = QString("%1/Monero/wallets").arg(this->configRoot);
|
|
|
|
this->defaultWalletDirRoot = QString("%1/Monero").arg(this->configRoot);
|
|
|
|
#elif defined(Q_OS_WIN)
|
2020-10-09 16:33:51 +00:00
|
|
|
this->defaultWalletDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/Monero";
|
|
|
|
this->defaultWalletDirRoot = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
2020-10-07 10:36:04 +00:00
|
|
|
#endif
|
|
|
|
|
2020-10-09 16:33:51 +00:00
|
|
|
// Create wallet dirs
|
2020-10-07 10:36:04 +00:00
|
|
|
if (!QDir().mkpath(defaultWalletDir))
|
|
|
|
qCritical() << "Unable to create dir: " << defaultWalletDir;
|
|
|
|
|
|
|
|
this->configDirectory = QString("%1/.config/feather/").arg(this->configRoot);
|
|
|
|
#if defined(Q_OS_UNIX)
|
|
|
|
if(!this->configDirectory.endsWith('/'))
|
|
|
|
this->configDirectory = QString("%1/").arg(this->configDirectory);
|
|
|
|
#endif
|
|
|
|
this->sorry();
|
|
|
|
|
|
|
|
// Config
|
|
|
|
createConfigDirectory(this->configDirectory);
|
|
|
|
|
|
|
|
if(this->cmdargs->isSet("stagenet"))
|
|
|
|
this->networkType = NetworkType::STAGENET;
|
|
|
|
else if(this->cmdargs->isSet("testnet"))
|
|
|
|
this->networkType = NetworkType::TESTNET;
|
|
|
|
else
|
|
|
|
this->networkType = NetworkType::MAINNET;
|
|
|
|
|
|
|
|
// auto nodeSourceUInt = config()->get(Config::nodeSource).toUInt();
|
|
|
|
// AppContext::nodeSource = static_cast<NodeSource>(nodeSourceUInt);
|
|
|
|
this->nodes = new Nodes(this, this->networkClearnet);
|
|
|
|
connect(this, &AppContext::nodeSourceChanged, this->nodes, &Nodes::onNodeSourceChanged);
|
|
|
|
connect(this, &AppContext::setCustomNodes, this->nodes, &Nodes::setCustomNodes);
|
|
|
|
connect(this, &AppContext::walletClosing, this->nodes, &Nodes::onWalletClosing);
|
|
|
|
|
|
|
|
// Tor & socks proxy
|
|
|
|
this->ws = new WSClient(this, m_wsUrl);
|
|
|
|
connect(this->ws, &WSClient::WSMessage, this, &AppContext::onWSMessage);
|
|
|
|
|
|
|
|
// timers
|
|
|
|
m_storeTimer->setSingleShot(true);
|
|
|
|
connect(this->m_storeTimer, &QTimer::timeout, [this](){
|
|
|
|
if (!this->currentWallet)
|
|
|
|
return;
|
|
|
|
qDebug() << "Storing wallet";
|
|
|
|
this->currentWallet->store();
|
|
|
|
});
|
|
|
|
|
|
|
|
// restore height lookup
|
|
|
|
this->initRestoreHeights();
|
|
|
|
|
|
|
|
// price history lookup
|
|
|
|
auto genesis_timestamp = this->restoreHeights[NetworkType::Type::MAINNET]->data.firstKey();
|
|
|
|
AppContext::txFiatHistory = new TxFiatHistory(genesis_timestamp, this->configDirectory);
|
|
|
|
connect(this->ws, &WSClient::connectionEstablished, AppContext::txFiatHistory, &TxFiatHistory::onUpdateDatabase);
|
|
|
|
connect(AppContext::txFiatHistory, &TxFiatHistory::requestYear, [=](unsigned int year){
|
|
|
|
QByteArray data = QString(R"({"cmd": "txFiatHistory", "data": {"year": %1}})").arg(year).toUtf8();
|
|
|
|
this->ws->sendMsg(data);
|
|
|
|
});
|
|
|
|
connect(AppContext::txFiatHistory, &TxFiatHistory::requestYearMonth, [=](unsigned int year, unsigned int month) {
|
|
|
|
QByteArray data = QString(R"({"cmd": "txFiatHistory", "data": {"year": %1, "month": %2}})").arg(year).arg(month).toUtf8();
|
|
|
|
this->ws->sendMsg(data);
|
|
|
|
});
|
|
|
|
|
|
|
|
// fiat/crypto lookup
|
|
|
|
AppContext::prices = new Prices();
|
|
|
|
|
|
|
|
// xmr.to
|
|
|
|
#if defined(XMRTO)
|
|
|
|
this->XMRTo = new XmrTo(this);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
this->walletManager = WalletManager::instance();
|
|
|
|
QString logPath = QString("%1/daemon.log").arg(configDirectory);
|
|
|
|
Monero::Utils::onStartup();
|
|
|
|
Monero::Wallet::init("", "feather", logPath.toStdString(), true);
|
|
|
|
|
|
|
|
bool logLevelFromEnv;
|
|
|
|
int logLevel = qEnvironmentVariableIntValue("MONERO_LOG_LEVEL", &logLevelFromEnv);
|
|
|
|
if(this->cmdargs->isSet("quiet"))
|
|
|
|
this->walletManager->setLogLevel(-1);
|
|
|
|
else if (logLevelFromEnv && logLevel >= 0 && logLevel <= Monero::WalletManagerFactory::LogLevel_Max)
|
|
|
|
Monero::WalletManagerFactory::setLogLevel(logLevel);
|
|
|
|
|
|
|
|
connect(this, &AppContext::createTransactionError, this, &AppContext::onCreateTransactionError);
|
|
|
|
|
|
|
|
// libwallet connects
|
|
|
|
connect(this->walletManager, &WalletManager::walletOpened, this, &AppContext::onWalletOpened);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::initTor() {
|
2020-10-09 00:13:08 +00:00
|
|
|
this->tor = new Tor(this, this);
|
2020-10-07 10:36:04 +00:00
|
|
|
this->tor->start();
|
|
|
|
|
|
|
|
if (!(isTails || isWhonix)) {
|
|
|
|
auto networkProxy = new QNetworkProxy(QNetworkProxy::Socks5Proxy, Tor::torHost, Tor::torPort);
|
|
|
|
this->network->setProxy(*networkProxy);
|
|
|
|
if (m_wsUrl.host().endsWith(".onion"))
|
|
|
|
this->ws->webSocket.setProxy(*networkProxy);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::initWS() {
|
|
|
|
this->ws->start();
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onCancelTransaction(PendingTransaction *tx, const QString &address) {
|
|
|
|
// tx cancelled by user
|
|
|
|
double amount = tx->amount() / AppContext::cdiv;
|
|
|
|
emit createTransactionCancelled(address, amount);
|
|
|
|
this->currentWallet->disposeTransaction(tx);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onSweepOutput(const QString &keyImage, QString address, bool churn, int outputs) const {
|
|
|
|
if(this->currentWallet == nullptr){
|
|
|
|
qCritical() << "Cannot create transaction; no wallet loaded";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (churn) {
|
|
|
|
address = this->currentWallet->address(0, 0); // primary address
|
|
|
|
}
|
|
|
|
|
|
|
|
qCritical() << "Creating transaction";
|
|
|
|
this->currentWallet->createTransactionSingleAsync(keyImage, address, outputs, this->tx_priority);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onCreateTransaction(XmrToOrder *order) {
|
|
|
|
// tx creation via xmr.to
|
|
|
|
const QString description = QString("XmrTo order %1").arg(order->uuid);
|
|
|
|
this->onCreateTransaction(order->receiving_subaddress, order->incoming_amount_total, description, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onCreateTransaction(const QString &address, const double amount, const QString &description, bool all) {
|
|
|
|
// tx creation
|
|
|
|
this->tmpTxDescription = description;
|
|
|
|
|
|
|
|
if(this->currentWallet == nullptr) {
|
|
|
|
emit createTransactionError("Cannot create transaction; no wallet loaded");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!all && amount <= 0) {
|
|
|
|
emit createTransactionError("Cannot send nothing");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto balance = this->currentWallet->balance() / AppContext::cdiv;
|
|
|
|
auto unlocked_balance = this->currentWallet->unlockedBalance() / AppContext::cdiv;
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto amount_num = static_cast<quint64>(amount * AppContext::cdiv);
|
|
|
|
qDebug() << "creating tx";
|
|
|
|
if(all || amount == balance)
|
|
|
|
this->currentWallet->createTransactionAllAsync(address, "", this->tx_mixin, this->tx_priority);
|
|
|
|
else
|
|
|
|
this->currentWallet->createTransactionAsync(address, "", amount_num, this->tx_mixin, this->tx_priority);
|
|
|
|
|
|
|
|
emit initiateTransaction();
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onCreateTransactionError(const QString &msg) {
|
|
|
|
this->tmpTxDescription = "";
|
|
|
|
emit endTransaction();
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::walletClose(bool emitClosedSignal) {
|
|
|
|
this->nodes->stopTimer();
|
|
|
|
if(this->currentWallet == nullptr) return;
|
|
|
|
emit walletClosing();
|
|
|
|
//ctx->currentWallet->store(); @TODO: uncomment to store on wallet close
|
|
|
|
this->currentWallet->disconnect();
|
|
|
|
this->walletManager->closeWallet();
|
|
|
|
if(this->currentWallet != nullptr)
|
|
|
|
this->currentWallet = nullptr;
|
|
|
|
if(emitClosedSignal)
|
|
|
|
emit walletClosed();
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onOpenWallet(const QString &path, const QString &password){
|
|
|
|
if(this->currentWallet != nullptr){
|
|
|
|
emit walletOpenedError("There is an active wallet opened.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!Utils::fileExists(path)) {
|
|
|
|
emit walletOpenedError(QString("Wallet not found: %1").arg(path));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
config()->set(Config::firstRun, false);
|
|
|
|
|
|
|
|
this->walletPath = path;
|
|
|
|
this->walletManager->openWalletAsync(path, password, this->networkType, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onPreferredFiatCurrencyChanged(const QString &symbol) {
|
|
|
|
if(this->currentWallet) {
|
|
|
|
auto *model = this->currentWallet->transactionHistoryModel();
|
|
|
|
if(model != nullptr) {
|
|
|
|
model->preferredFiatSign = AppContext::prices->fiat[symbol];
|
|
|
|
model->preferredFiatSymbol = symbol;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onWalletOpened(Wallet *wallet) {
|
|
|
|
auto state = wallet->status();
|
|
|
|
if (state != Wallet::Status_Ok) {
|
|
|
|
auto errMsg = wallet->errorString();
|
|
|
|
if(errMsg == QString("basic_string::_M_replace_aux") || errMsg == QString("std::bad_alloc")) {
|
|
|
|
qCritical() << errMsg;
|
|
|
|
this->walletManager->clearWalletCache(this->walletPath);
|
|
|
|
errMsg = QString("%1\n\nAttempted to clean wallet cache. Please restart Feather.").arg(errMsg);
|
|
|
|
this->walletClose(false);
|
|
|
|
emit walletOpenedError(errMsg);
|
|
|
|
} else if(errMsg.contains("wallet cannot be opened as")) {
|
|
|
|
this->walletClose(false);
|
|
|
|
emit walletOpenedError(errMsg);
|
|
|
|
} else if(errMsg.contains("is opened by another wallet program")) {
|
|
|
|
this->walletClose(false);
|
|
|
|
emit walletOpenedError(errMsg);
|
|
|
|
} else {
|
|
|
|
this->walletClose(false);
|
|
|
|
emit walletOpenPasswordNeeded(this->walletPassword.isEmpty());
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this->currentWallet = wallet;
|
|
|
|
this->walletPath = this->currentWallet->path() + ".keys";
|
2020-10-14 20:12:32 +00:00
|
|
|
this->walletViewOnly = this->currentWallet->viewOnly();
|
2020-10-07 10:36:04 +00:00
|
|
|
config()->set(Config::walletPath, this->walletPath);
|
|
|
|
|
|
|
|
connect(this->currentWallet, &Wallet::moneySpent, this, &AppContext::onMoneySpent);
|
|
|
|
connect(this->currentWallet, &Wallet::moneyReceived, this, &AppContext::onMoneyReceived);
|
|
|
|
connect(this->currentWallet, &Wallet::unconfirmedMoneyReceived, this, &AppContext::onUnconfirmedMoneyReceived);
|
|
|
|
connect(this->currentWallet, &Wallet::newBlock, this, &AppContext::onWalletNewBlock);
|
|
|
|
connect(this->currentWallet, &Wallet::updated, this, &AppContext::onWalletUpdate);
|
|
|
|
connect(this->currentWallet, &Wallet::refreshed, this, &AppContext::onWalletRefreshed);
|
|
|
|
connect(this->currentWallet, &Wallet::transactionCommitted, this, &AppContext::onTransactionCommitted);
|
|
|
|
connect(this->currentWallet, &Wallet::heightRefreshed, this, &AppContext::onHeightRefreshed);
|
|
|
|
connect(this->currentWallet, &Wallet::transactionCreated, this, &AppContext::onTransactionCreated);
|
|
|
|
connect(this->currentWallet, &Wallet::connectionStatusChanged, this, &AppContext::onConnectionStatusChanged);
|
|
|
|
|
|
|
|
emit walletOpened();
|
2020-10-15 00:19:13 +00:00
|
|
|
|
|
|
|
this->nodes->connectToNode();
|
2020-10-07 10:36:04 +00:00
|
|
|
this->updateBalance();
|
|
|
|
|
|
|
|
#ifdef DONATE_BEG
|
|
|
|
this->donateBeg();
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// force trigger preferredFiat signal for history model
|
|
|
|
this->onPreferredFiatCurrencyChanged(config()->get(Config::preferredFiatCurrency).toString());
|
2020-10-14 20:12:32 +00:00
|
|
|
this->setWindowTitle();
|
|
|
|
}
|
2020-10-12 22:01:06 +00:00
|
|
|
|
2020-10-14 20:12:32 +00:00
|
|
|
void AppContext::setWindowTitle(bool mining) {
|
2020-10-12 22:01:06 +00:00
|
|
|
QFileInfo fileInfo(this->walletPath);
|
|
|
|
auto title = QString("Feather - [%1]").arg(fileInfo.fileName());
|
2020-10-14 20:12:32 +00:00
|
|
|
if(this->walletViewOnly)
|
2020-10-12 22:01:06 +00:00
|
|
|
title += " [view-only]";
|
2020-10-14 20:12:32 +00:00
|
|
|
if(mining)
|
|
|
|
title += " [mining]";
|
|
|
|
|
2020-10-12 22:01:06 +00:00
|
|
|
emit setTitle(title);
|
2020-10-07 10:36:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onWSMessage(const QJsonObject &msg) {
|
|
|
|
QString cmd = msg.value("cmd").toString();
|
|
|
|
|
|
|
|
if(cmd == "blockheights") {
|
|
|
|
auto heights = msg.value("data").toObject();
|
|
|
|
auto mainnet = heights.value("mainnet").toInt();
|
|
|
|
auto stagenet = heights.value("stagenet").toInt();
|
|
|
|
auto changed = false;
|
|
|
|
|
|
|
|
if(!this->heights.contains("mainnet")) {
|
|
|
|
this->heights["mainnet"] = (unsigned int) mainnet;
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (mainnet > this->heights["mainnet"]) {
|
|
|
|
this->heights["mainnet"] = (unsigned int) mainnet;
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(!this->heights.contains("stagenet")) {
|
|
|
|
this->heights["stagenet"] = (unsigned int) stagenet;
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (stagenet > this->heights["stagenet"]) {
|
|
|
|
this->heights["stagenet"] = (unsigned int) stagenet;
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(changed)
|
|
|
|
emit blockHeightWSUpdated(this->heights);
|
|
|
|
}
|
|
|
|
|
|
|
|
else if(cmd == "nodes") {
|
|
|
|
this->onWSNodes(msg.value("data").toArray());
|
|
|
|
}
|
2020-10-14 20:12:32 +00:00
|
|
|
#if defined(MINING)
|
|
|
|
else if(cmd == "xmrig") {
|
|
|
|
this->XMRigDownloads(msg.value("data").toObject());
|
|
|
|
}
|
|
|
|
#endif
|
2020-10-07 10:36:04 +00:00
|
|
|
else if(cmd == "crypto_rates") {
|
|
|
|
QJsonArray crypto_rates = msg.value("data").toArray();
|
|
|
|
AppContext::prices->cryptoPricesReceived(crypto_rates);
|
|
|
|
}
|
|
|
|
|
|
|
|
else if(cmd == "fiat_rates") {
|
|
|
|
QJsonObject fiat_rates = msg.value("data").toObject();
|
|
|
|
AppContext::prices->fiatPricesReceived(fiat_rates);
|
|
|
|
}
|
|
|
|
#if defined(XMRTO)
|
|
|
|
else if(cmd == "xmrto_rates") {
|
|
|
|
auto xmr_rates = msg.value("data").toObject();
|
|
|
|
this->XMRTo->onRatesReceived(xmr_rates);
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
else if(cmd == "reddit") {
|
|
|
|
QJsonArray reddit_data = msg.value("data").toArray();
|
|
|
|
this->onWSReddit(reddit_data);
|
|
|
|
}
|
|
|
|
|
|
|
|
else if(cmd == "ccs") {
|
|
|
|
auto ccs_data = msg.value("data").toArray();
|
|
|
|
this->onWSCCS(ccs_data);
|
|
|
|
}
|
|
|
|
|
|
|
|
else if(cmd == "txFiatHistory") {
|
|
|
|
auto txFiatHistory_data = msg.value("data").toObject();
|
|
|
|
AppContext::txFiatHistory->onWSData(txFiatHistory_data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onWSNodes(const QJsonArray &nodes) {
|
|
|
|
QList<QSharedPointer<FeatherNode>> l;
|
|
|
|
for (auto &&entry: nodes) {
|
|
|
|
auto obj = entry.toObject();
|
|
|
|
auto nettype = obj.value("nettype");
|
|
|
|
auto type = obj.value("type");
|
|
|
|
|
|
|
|
// filter remote node network types
|
|
|
|
if(nettype == "mainnet" && this->networkType != NetworkType::MAINNET)
|
|
|
|
continue;
|
|
|
|
if(nettype == "stagenet" && this->networkType != NetworkType::STAGENET)
|
|
|
|
continue;
|
|
|
|
if(nettype == "testnet" && this->networkType != NetworkType::TESTNET)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if(type == "clearnet" && (this->isTails || this->isWhonix || this->isTorSocks))
|
|
|
|
continue;
|
|
|
|
if(type == "tor" && (!(this->isTails || this->isWhonix || this->isTorSocks)))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
auto node = new FeatherNode(
|
|
|
|
obj.value("address").toString(),
|
|
|
|
(unsigned int)obj.value("height").toInt(),
|
|
|
|
obj.value("online").toBool());
|
|
|
|
QSharedPointer<FeatherNode> r = QSharedPointer<FeatherNode>(node);
|
|
|
|
l.append(r);
|
|
|
|
}
|
|
|
|
this->nodes->onWSNodesReceived(l);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onWSReddit(const QJsonArray& reddit_data) {
|
|
|
|
QList<QSharedPointer<RedditPost>> l;
|
|
|
|
|
|
|
|
for (auto &&entry: reddit_data) {
|
|
|
|
auto obj = entry.toObject();
|
|
|
|
auto redditPost = new RedditPost(
|
|
|
|
obj.value("title").toString(),
|
|
|
|
obj.value("author").toString(),
|
|
|
|
obj.value("url").toString(),
|
|
|
|
obj.value("comments").toInt());
|
|
|
|
QSharedPointer<RedditPost> r = QSharedPointer<RedditPost>(redditPost);
|
|
|
|
l.append(r);
|
|
|
|
}
|
|
|
|
|
|
|
|
emit redditUpdated(l);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onWSCCS(const QJsonArray &ccs_data) {
|
|
|
|
QList<QSharedPointer<CCSEntry>> l;
|
|
|
|
|
|
|
|
|
|
|
|
QStringList fonts = {"state", "address", "author", "date",
|
|
|
|
"title", "target_amount", "raised_amount",
|
|
|
|
"percentage_funded", "contributions"};
|
|
|
|
|
|
|
|
for (auto &&entry: ccs_data) {
|
|
|
|
auto obj = entry.toObject();
|
|
|
|
auto c = QSharedPointer<CCSEntry>(new CCSEntry());
|
|
|
|
|
|
|
|
if (obj.value("state").toString() != "FUNDING-REQUIRED")
|
|
|
|
continue;
|
|
|
|
|
|
|
|
c->state = obj.value("state").toString();
|
|
|
|
c->address = obj.value("address").toString();
|
|
|
|
c->author = obj.value("author").toString();
|
|
|
|
c->date = obj.value("date").toString();
|
|
|
|
c->title = obj.value("title").toString();
|
|
|
|
c->url = obj.value("url").toString();
|
|
|
|
c->target_amount = obj.value("target_amount").toDouble();
|
|
|
|
c->raised_amount = obj.value("raised_amount").toDouble();
|
|
|
|
c->percentage_funded = obj.value("percentage_funded").toDouble();
|
|
|
|
c->contributions = obj.value("contributions").toInt();
|
|
|
|
l.append(c);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(l.count() == 0)
|
|
|
|
emit ccsEmpty();
|
|
|
|
|
|
|
|
emit ccsUpdated(l);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::createConfigDirectory(const QString &dir) {
|
|
|
|
if(!Utils::dirExists(dir)) {
|
|
|
|
qDebug() << QString("Creating directory: %1").arg(dir);
|
|
|
|
if(!QDir().mkpath(dir))
|
|
|
|
throw std::runtime_error("Could not create directory " + dir.toStdString());
|
|
|
|
}
|
|
|
|
|
|
|
|
QString config_dir_tor = QString("%1%2").arg(dir).arg("tor");
|
|
|
|
if(!Utils::dirExists(config_dir_tor)) {
|
|
|
|
qDebug() << QString("Creating directory: %1").arg(config_dir_tor);
|
|
|
|
if (!QDir().mkpath(config_dir_tor))
|
|
|
|
throw std::runtime_error("Could not create directory " + config_dir_tor.toStdString());
|
|
|
|
}
|
|
|
|
|
|
|
|
QString config_dir_tordata = QString("%1%2").arg(dir).arg("tor/data");
|
|
|
|
if(!Utils::dirExists(config_dir_tordata)) {
|
|
|
|
qDebug() << QString("Creating directory: %1").arg(config_dir_tordata);
|
|
|
|
if (!QDir().mkpath(config_dir_tordata))
|
|
|
|
throw std::runtime_error("Could not create directory " + config_dir_tordata.toStdString());
|
|
|
|
}
|
|
|
|
|
|
|
|
QString config_dir_torsocks = QString("%1%2").arg(dir).arg("torsocks");
|
|
|
|
if(!Utils::dirExists(config_dir_torsocks)) {
|
|
|
|
qDebug() << QString("Creating directory: %1").arg(config_dir_torsocks);
|
|
|
|
if (!QDir().mkpath(config_dir_torsocks))
|
|
|
|
throw std::runtime_error("Could not create directory " + config_dir_torsocks.toStdString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::createWallet(FeatherSeed seed, const QString &path, const QString &password) {
|
|
|
|
if(Utils::fileExists(path)) {
|
|
|
|
auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path);
|
|
|
|
qCritical() << err;
|
|
|
|
emit walletCreatedError(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-11 15:08:34 +00:00
|
|
|
if(seed.mnemonicSeed.isEmpty()) {
|
|
|
|
emit walletCreatedError("Mnemonic seed error. Failed to write wallet.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-07 10:36:04 +00:00
|
|
|
this->currentWallet = seed.writeWallet(this->walletManager, this->networkType, path, password, this->kdfRounds);
|
|
|
|
if(this->currentWallet == nullptr) {
|
|
|
|
emit walletCreatedError("Failed to write wallet");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-12 22:01:06 +00:00
|
|
|
this->createWalletFinish(password);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::createWalletViewOnly(const QString &path, const QString &password, const QString &address, const QString &viewkey, const QString &spendkey, quint64 restoreHeight) {
|
|
|
|
if(Utils::fileExists(path)) {
|
|
|
|
auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path);
|
|
|
|
qCritical() << err;
|
|
|
|
emit walletCreatedError(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!this->walletManager->addressValid(address, this->networkType)) {
|
|
|
|
auto err = QString("Failed to create wallet. Invalid address provided.").arg(path);
|
|
|
|
qCritical() << err;
|
|
|
|
emit walletCreatedError(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!this->walletManager->keyValid(viewkey, address, true, this->networkType)) {
|
|
|
|
auto err = QString("Failed to create wallet. Invalid viewkey provided.").arg(path);
|
|
|
|
qCritical() << err;
|
|
|
|
emit walletCreatedError(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!spendkey.isEmpty() && !this->walletManager->keyValid(spendkey, address, false, this->networkType)) {
|
|
|
|
auto err = QString("Failed to create wallet. Invalid spendkey provided.").arg(path);
|
|
|
|
qCritical() << err;
|
|
|
|
emit walletCreatedError(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this->currentWallet = this->walletManager->createWalletFromKeys(path, this->seedLanguage, this->networkType, address, viewkey, spendkey, restoreHeight);
|
|
|
|
this->createWalletFinish(password);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::createWalletFinish(const QString &password) {
|
2020-10-07 10:36:04 +00:00
|
|
|
this->currentWallet->setPassword(password);
|
|
|
|
this->currentWallet->store();
|
|
|
|
this->walletPassword = password;
|
|
|
|
emit walletCreated(this->currentWallet);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::initRestoreHeights() {
|
|
|
|
restoreHeights[NetworkType::TESTNET] = new RestoreHeightLookup(NetworkType::TESTNET);
|
|
|
|
restoreHeights[NetworkType::STAGENET] = RestoreHeightLookup::fromFile(":/assets/restore_heights_monero_stagenet.txt", NetworkType::STAGENET);
|
|
|
|
restoreHeights[NetworkType::MAINNET] = RestoreHeightLookup::fromFile(":/assets/restore_heights_monero_mainnet.txt", NetworkType::MAINNET);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onSetRestoreHeight(unsigned int height){
|
|
|
|
auto seed = this->currentWallet->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->currentWallet->setWalletCreationHeight(height);
|
|
|
|
this->currentWallet->setPassword(this->walletPassword); // trigger .keys write
|
|
|
|
|
|
|
|
// nuke wallet cache
|
|
|
|
const auto fn = this->currentWallet->path();
|
|
|
|
this->walletManager->clearWalletCache(fn);
|
|
|
|
|
|
|
|
emit customRestoreHeightSet(height);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onOpenAliasResolve(const QString &openAlias) {
|
|
|
|
// @TODO: calling this freezes for about 1-2 seconds :/
|
|
|
|
const auto result = this->walletManager->resolveOpenAlias(openAlias);
|
|
|
|
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 = this->walletManager->addressValid(address, this->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);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::donateBeg() {
|
2020-10-12 22:01:06 +00:00
|
|
|
if(this->currentWallet == nullptr) return;
|
|
|
|
if(this->networkType != NetworkType::Type::MAINNET) return;
|
|
|
|
if(this->currentWallet->viewOnly()) return;
|
2020-10-07 10:36:04 +00:00
|
|
|
|
|
|
|
auto donationCounter = config()->get(Config::donateBeg).toInt();
|
|
|
|
if(donationCounter == -1)
|
|
|
|
return; // previously donated
|
|
|
|
|
|
|
|
donationCounter += 1;
|
|
|
|
if (donationCounter % m_donationBoundary == 0)
|
|
|
|
emit donationNag();
|
|
|
|
config()->set(Config::donateBeg, donationCounter);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::sorry() {
|
|
|
|
auto msg = "Unable to start Feather, error code 0xd34db33f. If this problem "
|
|
|
|
"persists, please contact Technical Support.";
|
|
|
|
QStringList paths = {"C:\\ProgramData\\ryo", this->homeDir + "/.ryo"};
|
|
|
|
for(const QString &ryo: paths)
|
|
|
|
if(Utils::dirExists(ryo))
|
|
|
|
throw std::runtime_error(msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
AppContext::~AppContext() {
|
|
|
|
this->walletClose(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ############################################## LIBWALLET QT #########################################################
|
|
|
|
|
|
|
|
void AppContext::onMoneySpent(const QString &txId, quint64 amount) {
|
|
|
|
auto amount_num = amount / AppContext::cdiv;
|
|
|
|
qDebug() << Q_FUNC_INFO << txId << " " << QString::number(amount_num);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onMoneyReceived(const QString &txId, quint64 amount) {
|
|
|
|
// Incoming tx included in a block.
|
|
|
|
auto amount_num = amount / AppContext::cdiv;
|
|
|
|
qDebug() << Q_FUNC_INFO << txId << " " << QString::number(amount_num);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onUnconfirmedMoneyReceived(const QString &txId, quint64 amount) {
|
|
|
|
// Incoming transaction in pool
|
|
|
|
auto amount_num = amount / AppContext::cdiv;
|
|
|
|
qDebug() << Q_FUNC_INFO << txId << " " << QString::number(amount_num);
|
|
|
|
|
|
|
|
if(this->currentWallet->synchronized()) {
|
|
|
|
auto notify = QString("%1 XMR (pending)").arg(amount_num);
|
|
|
|
Utils::desktopNotify("Payment received", notify, 5000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onWalletUpdate() {
|
|
|
|
if (this->currentWallet->synchronized()) {
|
|
|
|
this->refreshModels();
|
|
|
|
}
|
|
|
|
|
|
|
|
this->updateBalance();
|
|
|
|
this->storeWallet();
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onWalletRefreshed() {
|
|
|
|
if (!this->refreshed) {
|
|
|
|
refreshModels();
|
|
|
|
this->refreshed = true;
|
|
|
|
this->storeWallet();
|
|
|
|
}
|
|
|
|
|
|
|
|
this->currentWallet->refreshHeightAsync();
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onWalletNewBlock(quint64 blockheight, quint64 targetHeight) {
|
|
|
|
this->syncStatusUpdated(blockheight, targetHeight);
|
|
|
|
|
|
|
|
if (this->currentWallet->synchronized()) {
|
|
|
|
this->currentWallet->coins()->refreshUnlocked();
|
|
|
|
this->currentWallet->history()->refresh(this->currentWallet->currentSubaddressAccount());
|
|
|
|
// Todo: only refresh tx confirmations
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onHeightRefreshed(quint64 walletHeight, quint64 daemonHeight, quint64 targetHeight) {
|
|
|
|
qDebug() << Q_FUNC_INFO << walletHeight << daemonHeight << targetHeight;
|
|
|
|
|
|
|
|
if (!this->currentWallet->connected())
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (daemonHeight < targetHeight) {
|
|
|
|
emit blockchainSync(daemonHeight, targetHeight);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this->syncStatusUpdated(walletHeight, daemonHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onTransactionCreated(PendingTransaction *tx, const QString &address, const QString &paymentId, quint32 mixin) {
|
|
|
|
if(address == this->featherDonationAddress)
|
|
|
|
this->featherDonationSending = true;
|
|
|
|
|
|
|
|
// tx created, but not sent yet. ask user to verify first.
|
|
|
|
emit createTransactionSuccess(tx, address, mixin);
|
|
|
|
emit endTransaction();
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList& txid){
|
|
|
|
this->currentWallet->history()->refresh(this->currentWallet->currentSubaddressAccount());
|
|
|
|
this->currentWallet->coins()->refresh(this->currentWallet->currentSubaddressAccount());
|
|
|
|
this->storeWallet();
|
|
|
|
emit transactionCommitted(status, tx, txid);
|
|
|
|
|
|
|
|
// this tx was a donation to Feather, stop our nagging
|
|
|
|
if(this->featherDonationSending) {
|
|
|
|
this->featherDonationSending = false;
|
|
|
|
config()->set(Config::donateBeg, -1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::onConnectionStatusChanged(int status) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::storeWallet() {
|
|
|
|
if (m_storeTimer->isActive())
|
|
|
|
return;
|
|
|
|
|
|
|
|
m_storeTimer->start(60000);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::updateBalance() {
|
|
|
|
if(!this->currentWallet)
|
|
|
|
throw std::runtime_error("this should not happen, ever");
|
|
|
|
|
|
|
|
AppContext::balance = this->currentWallet->balance() / AppContext::cdiv;
|
2020-10-09 19:38:33 +00:00
|
|
|
auto balance_str = QString::number(balance, 'f');
|
2020-10-07 10:36:04 +00:00
|
|
|
|
|
|
|
double unlocked = this->currentWallet->unlockedBalance() / AppContext::cdiv;
|
2020-10-09 19:38:33 +00:00
|
|
|
auto unlocked_str = QString::number(unlocked, 'f');
|
2020-10-07 10:36:04 +00:00
|
|
|
|
|
|
|
emit balanceUpdated(balance, unlocked, balance_str, unlocked_str);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::syncStatusUpdated(quint64 height, quint64 target) {
|
|
|
|
if (height < (target - 1)) {
|
|
|
|
emit refreshSync(height, target);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this->updateBalance();
|
|
|
|
emit synchronized();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AppContext::refreshModels() {
|
|
|
|
if (!this->currentWallet)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this->currentWallet->history()->refresh(this->currentWallet->currentSubaddressAccount());
|
|
|
|
this->currentWallet->subaddress()->refresh(this->currentWallet->currentSubaddressAccount());
|
|
|
|
this->currentWallet->coins()->refresh(this->currentWallet->currentSubaddressAccount());
|
|
|
|
// Todo: set timer for refreshes
|
|
|
|
}
|