Hush lite wallet
https://faq.hush.is/sdl
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2865 lines
99 KiB
2865 lines
99 KiB
// Copyright 2019-2022 The Hush developers
|
|
// Released under the GPLv3
|
|
#include "mainwindow.h"
|
|
#include "addressbook.h"
|
|
#include "viewalladdresses.h"
|
|
#include "ui_encryption.h"
|
|
#include "ui_mainwindow.h"
|
|
#include "ui_mobileappconnector.h"
|
|
#include "ui_addressbook.h"
|
|
#include "ui_privkey.h"
|
|
#include "ui_about.h"
|
|
#include "ui_settings.h"
|
|
#include "ui_viewalladdresses.h"
|
|
#include "controller.h"
|
|
#include "balancestablemodel.h"
|
|
#include "settings.h"
|
|
#include "version.h"
|
|
#include "connection.h"
|
|
#include "ui_sendHushTransactionChat.h"
|
|
#include "ui_contactrequest.h"
|
|
#include "ui_deposithush.h"
|
|
#include "ui_emoji.h"
|
|
#include "ui_requestContactDialog.h"
|
|
#include "chatmodel.h"
|
|
#include "requestdialog.h"
|
|
#include "ui_startupencryption.h"
|
|
#include "ui_removeencryption.h"
|
|
#include "ui_seedrestore.h"
|
|
#include "websockets.h"
|
|
#include "sodium.h"
|
|
#include "sodium/crypto_generichash_blake2b.h"
|
|
#include <QRegularExpression>
|
|
#include "FileSystem/FileSystem.h"
|
|
#include "Crypto/passwd.h"
|
|
#include "Crypto/FileEncryption.h"
|
|
#include "DataStore/DataStore.h"
|
|
#include "firsttimewizard.h"
|
|
#include "../lib/silentdragonlitelib.h"
|
|
#include <QCoreApplication>
|
|
#include <QGuiApplication>
|
|
#include <QKeyEvent>
|
|
|
|
using json = nlohmann::json;
|
|
|
|
#ifdef Q_OS_WIN
|
|
auto dirwallet = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("silentdragonlite/silentdragonlite-wallet.dat");
|
|
auto dirwalletenc = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("silentdragonlite/silentdragonlite-wallet-enc.dat");
|
|
auto dirwalletbackup = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("silentdragonlite/silentdragonlite-wallet.datBackup");
|
|
#endif
|
|
#ifdef Q_OS_MACOS
|
|
auto dirwallet = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("silentdragonlite/silentdragonlite-wallet.dat");
|
|
auto dirwalletenc = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("silentdragonlite/silentdragonlite-wallet-enc.dat");
|
|
auto dirwalletbackup = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("silentdragonlite/silentdragonlite-wallet.datBackup");
|
|
#endif
|
|
#ifdef Q_OS_LINUX
|
|
auto dirwallet = QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)).filePath(".silentdragonlite/silentdragonlite-wallet.dat");
|
|
auto dirwalletenc = QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)).filePath(".silentdragonlite/silentdragonlite-wallet-enc.dat");
|
|
auto dirwalletbackup = QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)).filePath(".silentdragonlite/silentdragonlite-wallet.datBackup");
|
|
#endif
|
|
|
|
MainWindow::MainWindow(QWidget *parent) :
|
|
QMainWindow(parent),
|
|
ui(new Ui::MainWindow)
|
|
{
|
|
qDebug() << __func__ << endl;
|
|
|
|
// Include css
|
|
QString theme_name;
|
|
try
|
|
{
|
|
theme_name = Settings::getInstance()->get_theme_name();
|
|
}
|
|
catch (...)
|
|
{
|
|
theme_name = "Dark";
|
|
}
|
|
|
|
this->slot_change_theme(theme_name);
|
|
|
|
ui->setupUi(this);
|
|
|
|
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
|
|
if (!dir.exists()){
|
|
qDebug() << __func__ << ": creating dir=" << dir.absolutePath();
|
|
QDir().mkpath(dir.absolutePath());
|
|
}else{}
|
|
|
|
logger = new Logger(this, QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("silentdragonlite-wallet.log"));
|
|
|
|
// Check for encryption
|
|
if(fileExists(dirwalletenc))
|
|
{
|
|
qDebug() << __func__ << ": decrypting wallet=" << dirwalletenc;
|
|
this->removeWalletEncryptionStartUp();
|
|
}
|
|
|
|
ui->memoTxtChat->setAutoFillBackground(false);
|
|
// TODO: make this HTML with some emoji
|
|
ui->memoTxtChat->setPlaceholderText("Send Memo (you can only write memo after the initial message from your contact)");
|
|
ui->memoTxtChat->setTextColor(Qt::white);
|
|
|
|
// Status Bar
|
|
setupStatusBar();
|
|
|
|
// Settings editor
|
|
setupSettingsModal();
|
|
|
|
// Set up exit action
|
|
QObject::connect(ui->actionExit, &QAction::triggered, this, &MainWindow::close);
|
|
|
|
// Telegram
|
|
QObject::connect(ui->actionTelegram, &QAction::triggered, this, &MainWindow::telegram);
|
|
|
|
// Website
|
|
QObject::connect(ui->actionWebsite, &QAction::triggered, this, &MainWindow::website);
|
|
|
|
// File a bug
|
|
QObject::connect(ui->actionFile_a_bug, &QAction::triggered, [=]() {
|
|
QDesktopServices::openUrl(QUrl("https://hush.is/tg_support"));
|
|
});
|
|
|
|
// Set up check for updates action
|
|
QObject::connect(ui->actionCheck_for_Updates, &QAction::triggered, [=] () {
|
|
// Silent is false, so show notification even if no update was found
|
|
rpc->checkForUpdate(false);
|
|
});
|
|
|
|
// Recurring payments
|
|
QObject::connect(ui->action_Recurring_Payments, &QAction::triggered, [=]() {
|
|
Recurring::getInstance()->showRecurringDialog(this);
|
|
});
|
|
|
|
// Request hush
|
|
QObject::connect(ui->actionRequest_hush, &QAction::triggered, [=]() {
|
|
RequestDialog::showRequesthush(this);
|
|
});
|
|
|
|
// Pay hush URI
|
|
QObject::connect(ui->actionPay_URI, &QAction::triggered, [=] () {
|
|
payhushURI();
|
|
});
|
|
|
|
// Wallet encryption
|
|
QObject::connect(ui->actionEncrypt_Wallet, &QAction::triggered, [=]() {
|
|
encryptWallet();
|
|
});
|
|
|
|
QObject::connect(ui->actionRemove_Wallet_Encryption, &QAction::triggered, [=]() {
|
|
removeWalletEncryption();
|
|
});
|
|
|
|
// Export All Private Keys
|
|
QObject::connect(ui->actionExport_All_Private_Keys, &QAction::triggered, this, &MainWindow::exportAllKeys);
|
|
|
|
// Backup wallet.dat
|
|
QObject::connect(ui->actionExport_Seed, &QAction::triggered, this, &MainWindow::exportSeed);
|
|
|
|
// Export transactions
|
|
QObject::connect(ui->actionExport_transactions, &QAction::triggered, this, &MainWindow::exportTransactions);
|
|
|
|
// Connect mobile app
|
|
QObject::connect(ui->actionConnect_Mobile_App, &QAction::triggered, this, [=] () {
|
|
if (rpc->getConnection() == nullptr)
|
|
return;
|
|
|
|
AppDataServer::getInstance()->connectAppDialog(this);
|
|
});
|
|
|
|
// Rescan
|
|
QObject::connect(ui->actionRescan, &QAction::triggered, [=]() {
|
|
|
|
Ui_Restore restoreSeed;
|
|
QDialog dialog(this);
|
|
restoreSeed.setupUi(&dialog);
|
|
Settings::saveRestore(&dialog);
|
|
|
|
rpc->fetchSeed([=](json reply) {
|
|
if (isJsonError(reply)) {
|
|
return;
|
|
}
|
|
|
|
restoreSeed.seed->setReadOnly(true);
|
|
restoreSeed.seed->setLineWrapMode(QPlainTextEdit::LineWrapMode::NoWrap);
|
|
QString seedJson = QString::fromStdString(reply["seed"].get<json::string_t>());
|
|
restoreSeed.seed->setPlainText(seedJson);
|
|
|
|
QString birthday = QString::number(reply["birthday"].get<json::number_unsigned_t>());
|
|
restoreSeed.birthday->setPlainText(birthday);
|
|
});
|
|
|
|
QObject::connect(restoreSeed.restore, &QPushButton::clicked, [&](){
|
|
|
|
QString seed = restoreSeed.seed->toPlainText();
|
|
if (seed.trimmed().split(" ").length() != 24) {
|
|
QMessageBox::warning(this, tr("Failed to restore wallet"),
|
|
tr("SilentDragonLite needs 24 words to restore wallet"),
|
|
QMessageBox::Ok);
|
|
return false;
|
|
}
|
|
|
|
|
|
// 2. Validate birthday
|
|
QString birthday_str = restoreSeed.birthday->toPlainText();
|
|
bool ok;
|
|
qint64 birthday = birthday_str.toUInt(&ok);
|
|
if (!ok) {
|
|
QMessageBox::warning(this, tr("Failed to parse wallet birthday"),
|
|
tr("Couldn't understand wallet birthday. This should be a block height from where to rescan the wallet. You can leave it as '0' if you don't know what it should be."),
|
|
QMessageBox::Ok);
|
|
return false;
|
|
}
|
|
|
|
|
|
QString number_str = restoreSeed.quantity->text();
|
|
qint64 number = number_str.toUInt();
|
|
|
|
auto config = std::shared_ptr<ConnectionConfig>(new ConnectionConfig());
|
|
config->server = Settings::getInstance()->getSettings().server;
|
|
// 3. Attempt to restore wallet with the seed phrase
|
|
{
|
|
char* resp = litelib_initialize_new_from_phrase(config->dangerous, config->server.toStdString().c_str(),
|
|
seed.toStdString().c_str(), birthday, number);
|
|
QString reply = litelib_process_response(resp);
|
|
|
|
if (reply.toUpper().trimmed() != "OK") {
|
|
QMessageBox::warning(this, tr("Failed to restore wallet"),
|
|
tr("Couldn't restore the wallet") + "\n" + reply,
|
|
QMessageBox::Ok);
|
|
|
|
}
|
|
}
|
|
|
|
// 4. Finally attempt to save the wallet
|
|
{
|
|
char* resp = litelib_execute("save", "");
|
|
QString reply = litelib_process_response(resp);
|
|
|
|
QByteArray ba_reply = reply.toUtf8();
|
|
QJsonDocument jd_reply = QJsonDocument::fromJson(ba_reply);
|
|
QJsonObject parsed = jd_reply.object();
|
|
|
|
if (parsed.isEmpty() || parsed["result"].isNull()) {
|
|
QMessageBox::warning(this, tr("Failed to save wallet"),
|
|
tr("Couldn't save the wallet") + "\n" + reply,
|
|
QMessageBox::Ok);
|
|
|
|
} else {
|
|
qDebug() << __func__ << ": saved wallet correctly";
|
|
}
|
|
|
|
dialog.close();
|
|
// To rescan, we clear the wallet state, and then reload the connection
|
|
// This will start a sync, and show the scanning status.
|
|
this->getRPC()->clearWallet([=] (auto) {
|
|
qDebug() << "Clearing wallet...";
|
|
// Save the wallet
|
|
this->getRPC()->saveWallet([=] (auto) {
|
|
qDebug() << "Saving wallet...";
|
|
// Then reload the connection. The ConnectionLoader deletes itself.
|
|
auto cl = new ConnectionLoader(this, rpc);
|
|
cl->loadConnection();
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
dialog.exec();
|
|
});
|
|
|
|
// Import Privkey
|
|
QObject::connect(ui->actionImport_Privatkey, &QAction::triggered, this, &MainWindow::importPrivKey);
|
|
// Address Book
|
|
QObject::connect(ui->action_Address_Book, &QAction::triggered, this, &MainWindow::addressBook);
|
|
|
|
// Set up about action
|
|
QObject::connect(ui->actionAbout, &QAction::triggered, [=] () {
|
|
QDialog aboutDialog(this);
|
|
Ui_about about;
|
|
about.setupUi(&aboutDialog);
|
|
Settings::saveRestore(&aboutDialog);
|
|
|
|
QString version = QString("Version ") % QString(APP_VERSION) % " (" % QString(__DATE__) % ")";
|
|
about.versionLabel->setText(version);
|
|
|
|
about.qtversion->setText( QString("QT Version ") % QString(QT_VERSION_STR) );
|
|
|
|
aboutDialog.exec();
|
|
});
|
|
|
|
// Initialize to the balances tab
|
|
ui->tabWidget->setCurrentIndex(0);
|
|
|
|
setupSendTab();
|
|
setupTransactionsTab();
|
|
setupReceiveTab();
|
|
setupBalancesTab();
|
|
setuphushdTab();
|
|
setupchatTab();
|
|
|
|
rpc = new Controller(this);
|
|
|
|
restoreSavedStates();
|
|
|
|
if (AppDataServer::getInstance()->isAppConnected()) {
|
|
qDebug() << __func__ << ": app is connected to wormhole";
|
|
auto ads = AppDataServer::getInstance();
|
|
|
|
QString wormholecode = "";
|
|
if (ads->getAllowInternetConnection())
|
|
wormholecode = ads->getWormholeCode(ads->getSecretHex());
|
|
|
|
createWebsocket(wormholecode);
|
|
}
|
|
}
|
|
|
|
bool MainWindow::fileExists(QString path)
|
|
{
|
|
QFileInfo check_file(path);
|
|
return (check_file.exists() && check_file.isFile());
|
|
}
|
|
|
|
void MainWindow::createWebsocket(QString wormholecode) {
|
|
qDebug() << "Listening for app connections on port 8777";
|
|
// Create the websocket server, for listening to direct connections
|
|
wsserver = new WSServer(8777, false, this);
|
|
|
|
if (!wormholecode.isEmpty()) {
|
|
// Connect to the wormhole service
|
|
wormhole = new WormholeClient(this, wormholecode);
|
|
}
|
|
}
|
|
|
|
void MainWindow::stopWebsocket() {
|
|
delete wsserver;
|
|
wsserver = nullptr;
|
|
|
|
delete wormhole;
|
|
wormhole = nullptr;
|
|
|
|
qDebug() << "Websockets for app connections shut down";
|
|
}
|
|
|
|
bool MainWindow::isWebsocketListening() {
|
|
return wsserver != nullptr;
|
|
}
|
|
|
|
void MainWindow::replaceWormholeClient(WormholeClient* newClient) {
|
|
delete wormhole;
|
|
wormhole = newClient;
|
|
}
|
|
|
|
void MainWindow::restoreSavedStates() {
|
|
QSettings s;
|
|
restoreGeometry(s.value("geometry").toByteArray());
|
|
|
|
auto balance_geom = s.value("baltablegeom");
|
|
if (balance_geom == QVariant()) {
|
|
ui->balancesTable->setColumnWidth(0, 500);
|
|
} else {
|
|
ui->balancesTable->horizontalHeader()->restoreState(balance_geom.toByteArray());
|
|
}
|
|
|
|
auto tx_geom = s.value("tratablegeom");
|
|
if (tx_geom == QVariant()) {
|
|
ui->transactionsTable->setColumnWidth(1, 500);
|
|
} else {
|
|
ui->transactionsTable->horizontalHeader()->restoreState(tx_geom.toByteArray());
|
|
}
|
|
}
|
|
|
|
void MainWindow::doClose() {
|
|
closeEvent(nullptr);
|
|
}
|
|
|
|
void MainWindow::doClosePw() {
|
|
closeEventpw(nullptr);
|
|
}
|
|
|
|
void MainWindow::closeEvent(QCloseEvent* event) {
|
|
QSettings s;
|
|
|
|
s.setValue("geometry", saveGeometry());
|
|
s.setValue("baltablegeom", ui->balancesTable->horizontalHeader()->saveState());
|
|
s.setValue("tratablegeom", ui->transactionsTable->horizontalHeader()->saveState());
|
|
|
|
s.sync();
|
|
|
|
// Let the RPC know to shut down any running service.
|
|
if(rpc){
|
|
rpc->shutdownhushd();
|
|
}
|
|
|
|
int passphraselength = DataStore::getChatDataStore()->getPassword().length();
|
|
|
|
// Check is encryption is ON for SDl
|
|
if(passphraselength > 0) {
|
|
// delete old file before
|
|
|
|
//auto dirHome = QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
|
|
QFile fileoldencryption(dirwalletenc);
|
|
fileoldencryption.remove();
|
|
|
|
// Encrypt our wallet.dat
|
|
QString passphraseHash = DataStore::getChatDataStore()->getPassword();
|
|
int length = passphraseHash.length();
|
|
|
|
char *sequence1 = NULL;
|
|
sequence1 = new char[length+1];
|
|
strncpy(sequence1, passphraseHash.toUtf8(), length+1);
|
|
|
|
#define PassphraseHashEnd ((const unsigned char *) sequence1)
|
|
#define MESSAGE_LEN length
|
|
|
|
#define PASSWORD sequence
|
|
#define KEY_LEN crypto_box_SEEDBYTES
|
|
|
|
const QByteArray ba = QByteArray::fromHex(passphraseHash.toLatin1());
|
|
const unsigned char *encryptedMemo1 = reinterpret_cast<const unsigned char *>(ba.constData());
|
|
|
|
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
|
|
QString sourceWallet_file = dirwallet;
|
|
QString target_encWallet_file = dirwalletenc;
|
|
|
|
// FileEncryption::encrypt(target_enc_file, source_file, key);
|
|
FileEncryption::encrypt(target_encWallet_file, sourceWallet_file, encryptedMemo1);
|
|
|
|
QFile wallet(dirwallet);
|
|
wallet.remove();
|
|
}
|
|
|
|
|
|
// Bubble up
|
|
if (event)
|
|
QMainWindow::closeEvent(event);
|
|
}
|
|
|
|
void MainWindow::closeEventpw(QCloseEvent* event) {
|
|
// Let the RPC know to shut down any running service.
|
|
qDebug() << __func__ << ": event=" << event << " this=" << this;
|
|
if (rpc) {
|
|
rpc->shutdownhushd();
|
|
} else {
|
|
qDebug() << __func__ << ": invalid rpc object!";
|
|
}
|
|
}
|
|
|
|
void MainWindow::encryptWallet() {
|
|
|
|
QDialog d(this);
|
|
Ui_encryptionDialog ed;
|
|
ed.setupUi(&d);
|
|
|
|
qDebug() << __func__ << ": start";
|
|
|
|
// Handle edits on the password box
|
|
|
|
auto fnPasswordEdited = [=](const QString&) {
|
|
// Enable the OK button if the passwords match.
|
|
QString password = ed.txtPassword->text();
|
|
|
|
if (!ed.txtPassword->text().isEmpty() &&
|
|
ed.txtPassword->text() == ed.txtConfirmPassword->text() && password.size() >= 16) {
|
|
ed.lblPasswordMatch->setText("");
|
|
ed.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
|
|
} else {
|
|
//ed.lblPasswordMatch->setText(tr("Passphrase don't match or You have entered too few letters (16 minimum)"));
|
|
ed.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
|
}
|
|
|
|
};
|
|
|
|
QObject::connect(ed.txtConfirmPassword, &QLineEdit::textChanged, fnPasswordEdited);
|
|
QObject::connect(ed.txtPassword, &QLineEdit::textChanged, fnPasswordEdited);
|
|
|
|
if (d.exec() == QDialog::Accepted)
|
|
{
|
|
|
|
QString passphraseBlank = ed.txtPassword->text(); // data comes from user inputs
|
|
QString passphrase = QString("HUSH3") + passphraseBlank + QString("SDL");
|
|
int length = passphrase.length();
|
|
|
|
|
|
char *sequence = NULL;
|
|
sequence = new char[length+1];
|
|
strncpy(sequence, passphrase.toUtf8(), length +1);
|
|
|
|
QString passphraseHash = blake3_PW(sequence);
|
|
DataStore::getChatDataStore()->setPassword(passphraseHash);
|
|
|
|
char *sequence1 = NULL;
|
|
sequence1 = new char[length+1];
|
|
strncpy(sequence1, passphraseHash.toUtf8(), length+1);
|
|
|
|
#define MESSAGE ((const unsigned char *) sequence)
|
|
#define MESSAGE_LEN length
|
|
#define hash ((const unsigned char *) sequence1)
|
|
|
|
#define PASSWORD sequence
|
|
#define KEY_LEN crypto_box_SEEDBYTES
|
|
|
|
unsigned char key[KEY_LEN];
|
|
|
|
if (crypto_pwhash(key, sizeof key, PASSWORD, strlen(PASSWORD), hash,
|
|
crypto_pwhash_OPSLIMIT_SENSITIVE, crypto_pwhash_MEMLIMIT_SENSITIVE,
|
|
crypto_pwhash_ALG_DEFAULT) != 0) {
|
|
/* out of memory */
|
|
QMessageBox::information(this, tr("Out of memory!"),
|
|
QString("Please close some other programs to free up memory and try again"),
|
|
QMessageBox::Ok
|
|
);
|
|
qDebug() << __func__ << ": out of memory!";
|
|
exit(1);
|
|
}
|
|
QString passphraseHash1 = QByteArray(reinterpret_cast<const char*>(key), KEY_LEN).toHex();
|
|
DataStore::getChatDataStore()->setPassword(passphraseHash1);
|
|
|
|
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
|
|
auto dirHome = QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
|
|
QString sourceWallet_file = dirwallet;
|
|
QString target_encWallet_file = dirwalletenc;
|
|
|
|
FileEncryption::encrypt(target_encWallet_file, sourceWallet_file, key);
|
|
|
|
QFile wallet(dirwallet);
|
|
wallet.rename(dirwalletbackup);
|
|
|
|
QMessageBox::information(this, tr("Wallet Encryption Success"),
|
|
QString("Successfully encrypted your wallet"),
|
|
QMessageBox::Ok
|
|
);
|
|
}
|
|
qDebug() << __func__ << ": finish";
|
|
}
|
|
|
|
void MainWindow::removeWalletEncryption() {
|
|
qDebug() << __func__ << ": removing wallet encryption";
|
|
QDialog d(this);
|
|
Ui_removeencryption ed;
|
|
ed.setupUi(&d);
|
|
|
|
qDebug() << __func__ << ": done with setupUi";
|
|
|
|
if (fileExists(dirwalletenc) == false) {
|
|
QMessageBox::information(this, tr("Wallet is not encrypted"),
|
|
tr("Your wallet is not encrypted with a passphrase."),
|
|
QMessageBox::Ok
|
|
);
|
|
qDebug() << __func__ << ": wallet=" << dirwalletenc << " does NOT exist";
|
|
return;
|
|
} else {
|
|
qDebug() << __func__ << ": wallet=" << dirwalletenc << " exists";
|
|
}
|
|
|
|
auto fnPasswordEdited = [=](const QString&) {
|
|
QString password = ed.txtPassword->text();
|
|
// Enable the OK button if the passwords match.
|
|
if (!ed.txtPassword->text().isEmpty() &&
|
|
ed.txtPassword->text() == ed.txtConfirmPassword->text() && password.size() >= 16) {
|
|
ed.lblPasswordMatch->setText("");
|
|
ed.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
|
|
} else {
|
|
ed.lblPasswordMatch->setText(tr("Passwords don't match"));
|
|
ed.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
|
}
|
|
|
|
};
|
|
|
|
QObject::connect(ed.txtConfirmPassword, &QLineEdit::textChanged, fnPasswordEdited);
|
|
QObject::connect(ed.txtPassword, &QLineEdit::textChanged, fnPasswordEdited);
|
|
|
|
qDebug() << __func__ << ": connected GUI events";
|
|
|
|
if (d.exec() == QDialog::Accepted) {
|
|
QString passphraseBlank = ed.txtPassword->text(); // data comes from user inputs
|
|
QString passphrase = QString("HUSH3") + passphraseBlank + QString("SDL");
|
|
|
|
int length = passphrase.length();
|
|
|
|
qDebug() << __func__ << ": Passphrase length = " << length;
|
|
|
|
char *sequence = NULL;
|
|
sequence = new char[length+1];
|
|
strncpy(sequence, passphrase.toUtf8(), length +1);
|
|
|
|
QString passphraseHash = blake3_PW(sequence);
|
|
|
|
char *sequence1 = NULL;
|
|
sequence1 = new char[length+1];
|
|
strncpy(sequence1, passphraseHash.toUtf8(), length+1);
|
|
|
|
#define hash ((const unsigned char *) sequence1)
|
|
#define PASSWORD sequence
|
|
#define KEY_LEN crypto_box_SEEDBYTES
|
|
|
|
unsigned char key[KEY_LEN];
|
|
|
|
if (crypto_pwhash(key, sizeof key, PASSWORD, strlen(PASSWORD), hash,
|
|
crypto_pwhash_OPSLIMIT_SENSITIVE, crypto_pwhash_MEMLIMIT_SENSITIVE, crypto_pwhash_ALG_DEFAULT) != 0) {
|
|
/* out of memory */
|
|
qDebug() << __func__ << ": crypto_pwhash failed! Possibly out of memory";
|
|
return;
|
|
}
|
|
|
|
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
|
|
auto dirHome = QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
|
|
QString target_encwallet_file = dirwalletenc;
|
|
QString target_decwallet_file = dirwallet;
|
|
|
|
FileEncryption::decrypt(target_decwallet_file, target_encwallet_file, key);
|
|
|
|
QFile filencrypted(dirwalletenc);
|
|
QFile wallet(dirwallet);
|
|
|
|
qDebug() << __func__ << ": wallet size=" << wallet.size();
|
|
|
|
if (wallet.size() > 0) {
|
|
QMessageBox::information(this, tr("Wallet decryption Success"),
|
|
QString("Successfully delete the encryption"),
|
|
QMessageBox::Ok
|
|
);
|
|
filencrypted.remove();
|
|
} else {
|
|
QMessageBox::critical(this, tr("Wallet Encryption Failed"),
|
|
QString("False password, please try again"),
|
|
QMessageBox::Ok
|
|
);
|
|
this->removeWalletEncryption();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void MainWindow::removeWalletEncryptionStartUp() {
|
|
qDebug() << __func__ << ": removing wallet encryption";
|
|
QDialog d(this);
|
|
Ui_startup ed;
|
|
ed.setupUi(&d);
|
|
|
|
// Connect cancel button to close app on queued connection
|
|
QObject::connect(ed.buttonBox, &QDialogButtonBox::rejected, qApp, &QCoreApplication::quit, Qt::QueuedConnection);
|
|
|
|
// Connect new/restore button click
|
|
QObject::connect(ed.new_restore, &QPushButton::clicked, [&] {
|
|
|
|
d.close();
|
|
QFile wallet(dirwallet);
|
|
QFile walletenc(dirwalletenc);
|
|
|
|
wallet.remove();
|
|
walletenc.remove();
|
|
|
|
auto cl = new ConnectionLoader(this, rpc);
|
|
cl->loadConnection();
|
|
});
|
|
|
|
if (d.exec() == QDialog::Accepted){
|
|
QString passphraseBlank = ed.txtPassword->text(); // data comes from user inputs
|
|
|
|
QString passphrase = QString("HUSH3") + passphraseBlank + QString("SDL");
|
|
int length = passphrase.length();
|
|
|
|
char *sequence = NULL;
|
|
sequence = new char[length+1];
|
|
strncpy(sequence, passphrase.toUtf8(), length +1);
|
|
|
|
QString passphraseHash = blake3_PW(sequence);
|
|
|
|
|
|
char *sequence1 = NULL;
|
|
sequence1 = new char[length+1];
|
|
strncpy(sequence1, passphraseHash.toUtf8(), length+1);
|
|
|
|
#define MESSAGE ((const unsigned char *) sequence)
|
|
#define MESSAGE_LEN length
|
|
#define hash ((const unsigned char *) sequence1)
|
|
|
|
#define PASSWORD sequence
|
|
#define KEY_LEN crypto_box_SEEDBYTES
|
|
|
|
unsigned char key[KEY_LEN];
|
|
|
|
if (crypto_pwhash(key, sizeof key, PASSWORD, strlen(PASSWORD), hash,
|
|
crypto_pwhash_OPSLIMIT_SENSITIVE, crypto_pwhash_MEMLIMIT_SENSITIVE,
|
|
crypto_pwhash_ALG_DEFAULT) != 0) {
|
|
/* out of memory */
|
|
qDebug() << __func__ << ": crypto_pwhash failed! Possibly out of memory";
|
|
return;
|
|
}
|
|
|
|
QString passphraseHash1 = QByteArray(reinterpret_cast<const char*>(key), KEY_LEN).toHex();
|
|
DataStore::getChatDataStore()->setPassword(passphraseHash1);
|
|
|
|
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
|
|
|
|
QString target_encwallet_file = dirwalletenc;
|
|
QString target_decwallet_file = dirwallet;
|
|
|
|
FileEncryption::decrypt(target_decwallet_file, target_encwallet_file, key);
|
|
|
|
auto dirHome = QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
|
|
QFile wallet(dirwallet);
|
|
qDebug() << __func__ << ": wallet size=" << wallet.size();
|
|
|
|
if (wallet.size() == 0) {
|
|
QMessageBox::critical(this, tr("Wallet Encryption Failed"),
|
|
QString("false password please try again"),
|
|
QMessageBox::Ok
|
|
);
|
|
this->removeWalletEncryptionStartUp();
|
|
}else{}
|
|
|
|
}else{
|
|
this->doClosePw();
|
|
}
|
|
}
|
|
|
|
QString MainWindow::getPassword()
|
|
{
|
|
|
|
return _password;
|
|
}
|
|
|
|
void MainWindow::setPassword(QString password)
|
|
{
|
|
|
|
_password = password;
|
|
}
|
|
|
|
QString MainWindow::getAmt()
|
|
{
|
|
|
|
return _amt;
|
|
}
|
|
|
|
void MainWindow::setAmt(QString amt)
|
|
{
|
|
|
|
_amt = amt;
|
|
}
|
|
|
|
QString MainWindow::getMoneyMemo()
|
|
{
|
|
|
|
return _moneymemo;
|
|
}
|
|
|
|
void MainWindow::setMoneyMemo(QString moneymemo)
|
|
{
|
|
|
|
_moneymemo = moneymemo;
|
|
}
|
|
|
|
void MainWindow::setupStatusBar() {
|
|
qDebug() << __func__ << endl;
|
|
// Status Bar
|
|
loadingLabel = new QLabel();
|
|
loadingMovie = new QMovie(":/icons/res/loading.gif");
|
|
loadingMovie->setScaledSize(QSize(32, 16));
|
|
loadingMovie->start();
|
|
loadingLabel->setAttribute(Qt::WA_NoSystemBackground);
|
|
loadingLabel->setMovie(loadingMovie);
|
|
|
|
|
|
ui->statusBar->addPermanentWidget(loadingLabel);
|
|
loadingLabel->setVisible(false);
|
|
|
|
// Custom status bar menu
|
|
ui->statusBar->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
QObject::connect(ui->statusBar, &QStatusBar::customContextMenuRequested, [=](QPoint pos) {
|
|
auto msg = ui->statusBar->currentMessage();
|
|
QMenu menu(this);
|
|
|
|
if (!msg.isEmpty() && msg.startsWith(Settings::txidStatusMessage)) {
|
|
auto txid = msg.split(":")[1].trimmed();
|
|
menu.addAction(tr("Copy txid"), [=]() {
|
|
QGuiApplication::clipboard()->setText(txid);
|
|
});
|
|
menu.addAction(tr("Copy block explorer link"), [=]() {
|
|
// auto explorer = Settings::getInstance()->getExplorer();
|
|
QGuiApplication::clipboard()->setText("https://explorer.hush.is/tx/" + txid);
|
|
});
|
|
|
|
menu.addAction(tr("View tx on block explorer"), [=]() {
|
|
Settings::openTxInExplorer(txid);
|
|
});
|
|
}
|
|
|
|
menu.addAction(tr("Refresh"), [=]() {
|
|
rpc->refresh(true);
|
|
});
|
|
QPoint gpos(mapToGlobal(pos).x(), mapToGlobal(pos).y() + this->height() - ui->statusBar->height());
|
|
menu.exec(gpos);
|
|
});
|
|
|
|
statusLabel = new QLabel();
|
|
ui->statusBar->addPermanentWidget(statusLabel);
|
|
|
|
statusIcon = new QLabel();
|
|
ui->statusBar->addPermanentWidget(statusIcon);
|
|
}
|
|
|
|
void MainWindow::setupSettingsModal() {
|
|
// Set up File -> Settings action
|
|
QObject::connect(ui->actionSettings, &QAction::triggered, [=]() {
|
|
QDialog settingsDialog(this);
|
|
Ui_Settings settings;
|
|
settings.setupUi(&settingsDialog);
|
|
Settings::saveRestore(&settingsDialog);
|
|
|
|
// Include currencies
|
|
|
|
QString currency_name;
|
|
try
|
|
{
|
|
currency_name = Settings::getInstance()->get_currency_name();
|
|
}
|
|
catch (...)
|
|
{
|
|
currency_name = "USD";
|
|
}
|
|
|
|
this->slot_change_currency(currency_name);
|
|
|
|
// Setup theme combo
|
|
int theme_index = settings.comboBoxTheme->findText(Settings::getInstance()->get_theme_name(), Qt::MatchExactly);
|
|
settings.comboBoxTheme->setCurrentIndex(theme_index);
|
|
|
|
QObject::connect(settings.comboBoxTheme, &QComboBox::currentTextChanged, [=] (QString theme_name) {
|
|
this->slot_change_theme(theme_name);
|
|
});
|
|
|
|
// Get Currency Data
|
|
|
|
int currency_index = settings.comboBoxCurrency->findText(Settings::getInstance()->get_currency_name(), Qt::MatchExactly);
|
|
settings.comboBoxCurrency->setCurrentIndex(currency_index);
|
|
|
|
QObject::connect(settings.comboBoxCurrency, &QComboBox::currentTextChanged, [=] (QString currency_name) {
|
|
this->slot_change_currency(currency_name);
|
|
rpc->refresh(true);
|
|
|
|
// Tell the user to restart
|
|
QMessageBox::information(this, tr("Currency Change"), tr("This change can take a few seconds."), QMessageBox::Ok);
|
|
});
|
|
|
|
// Check for updates
|
|
settings.chkCheckUpdates->setChecked(Settings::getInstance()->getCheckForUpdates());
|
|
|
|
// Fetch prices
|
|
settings.chkFetchPrices->setChecked(Settings::getInstance()->getAllowFetchPrices());
|
|
|
|
// List of default servers
|
|
settings.cmbServer->addItem("https://lite.hush.is");
|
|
settings.cmbServer->addItem("https://lite2.hush.is");
|
|
settings.cmbServer->addItem("https://lite.hush.land");
|
|
settings.cmbServer->addItem("https://wtfistheinternet.hush.is");
|
|
settings.cmbServer->addItem("https://poop.granitefone.me");
|
|
settings.cmbServer->addItem("https://lite.hushpool.is");
|
|
settings.cmbServer->addItem("https://lite2.hushpool.is");
|
|
|
|
//TODO: seperate lists of https/Tor servers, only show user or attempt
|
|
// connection to .onion if user has it enabled
|
|
//settings.cmbServer->addItem("6onaaujm4ozaokzu.onion:80");
|
|
|
|
// Load current values into the dialog
|
|
auto conf = Settings::getInstance()->getSettings();
|
|
settings.cmbServer->setCurrentText(conf.server);
|
|
|
|
// Connection tab by default
|
|
settings.tabWidget->setCurrentIndex(0);
|
|
|
|
if (settingsDialog.exec() == QDialog::Accepted) {
|
|
// Check for updates
|
|
Settings::getInstance()->setCheckForUpdates(settings.chkCheckUpdates->isChecked());
|
|
|
|
// Allow fetching prices
|
|
Settings::getInstance()->setAllowFetchPrices(settings.chkFetchPrices->isChecked());
|
|
|
|
// Save the server
|
|
bool reloadConnection = false;
|
|
if (conf.server != settings.cmbServer->currentText().trimmed()) {
|
|
reloadConnection = true;
|
|
}
|
|
Settings::getInstance()->saveSettings(settings.cmbServer->currentText().trimmed());
|
|
|
|
if (reloadConnection) {
|
|
// Save settings
|
|
Settings::getInstance()->saveSettings(settings.cmbServer->currentText());
|
|
|
|
// Save the wallet
|
|
getRPC()->saveWallet([=] (auto) {
|
|
// Then reload the connection. The ConnectionLoader deletes itself.
|
|
auto cl = new ConnectionLoader(this, rpc);
|
|
cl->loadConnection();
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void MainWindow::addressBook() {
|
|
// Check to see if there is a target.
|
|
QRegularExpression re("Address[0-9]+", QRegularExpression::CaseInsensitiveOption);
|
|
for (auto target: ui->sendToWidgets->findChildren<QLineEdit *>(re)) {
|
|
if (target->hasFocus()) {
|
|
AddressBook::open(this, target);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// If there was no target, then just run with no target.
|
|
AddressBook::open(this);
|
|
}
|
|
|
|
void MainWindow::telegram() {
|
|
QString url = "https://hush.is/telegram/";
|
|
QDesktopServices::openUrl(QUrl(url));
|
|
}
|
|
|
|
void MainWindow::website() {
|
|
QString url = "https://hush.is";
|
|
QDesktopServices::openUrl(QUrl(url));
|
|
}
|
|
|
|
void MainWindow::doImport(QList<QString>* keys) {
|
|
if (rpc->getConnection() == nullptr) {
|
|
// No connection, just return
|
|
return;
|
|
}
|
|
|
|
if (keys->isEmpty()) {
|
|
delete keys;
|
|
ui->statusBar->showMessage(tr("Private key import rescan in progress. Your funds will be shielded into this wallet and backed up by your seed phrase. This will take some time"));
|
|
return;
|
|
}
|
|
|
|
// Pop the first key
|
|
QString key = keys->first();
|
|
QString key1 = key + QString(" ") + QString("0");
|
|
keys->pop_front();
|
|
//bool rescan = keys->isEmpty();
|
|
|
|
if (key.startsWith("SK") || key.startsWith("secret")) {
|
|
rpc->importZPrivKey(key, [=] (auto) { this->doImport(keys); });
|
|
} else if (key.startsWith("U") || key.startsWith("5") || key.startsWith("L") || key.startsWith("K")) {
|
|
// 5 = uncompressed, len=51
|
|
// LK= compressed, len=52
|
|
// TODO: verify exact length of (un)compressed
|
|
if(key.length() > 52) {
|
|
QMessageBox::critical(this, tr("Wrong Private key format"),
|
|
tr("That private key is too long. It should be 51 or 52 characters.") + "\n");
|
|
return;
|
|
}
|
|
|
|
if(key.length() < 51) {
|
|
QMessageBox::critical(this, tr("Wrong Private key format"),
|
|
tr("That private key is too short. It should be 51 or 52 characters.") + "\n");
|
|
return;
|
|
}
|
|
rpc->importTPrivKey(key, [=] (auto) { this->doImport(keys); });
|
|
}else{
|
|
QMessageBox::critical(this, tr("Wrong Privatkey format"),
|
|
tr("Privatkey should start with 5, K, L or U (for taddr) or secret- (for zaddr)") + "\n");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Callback invoked when the RPC has finished loading all the balances, and the UI
|
|
// is now ready to send transactions.
|
|
void MainWindow::balancesReady() {
|
|
// First-time check
|
|
if (uiPaymentsReady)
|
|
return;
|
|
|
|
uiPaymentsReady = true;
|
|
qDebug() << "Payment UI now ready!";
|
|
|
|
// There is a pending URI payment (from the command line, or from a secondary instance),
|
|
// process it.
|
|
if (!pendingURIPayment.isEmpty()) {
|
|
qDebug() << "Paying hush URI";
|
|
payhushURI(pendingURIPayment);
|
|
pendingURIPayment = "";
|
|
}
|
|
|
|
// Execute any pending Recurring payments
|
|
Recurring::getInstance()->processPending(this);
|
|
}
|
|
|
|
// Event filter for MacOS specific handling of payment URIs
|
|
bool MainWindow::eventFilter(QObject *object, QEvent *event) {
|
|
if (event->type() == QEvent::FileOpen) {
|
|
QFileOpenEvent *fileEvent = static_cast<QFileOpenEvent*>(event);
|
|
if (!fileEvent->url().isEmpty())
|
|
payhushURI(fileEvent->url().toString());
|
|
|
|
return true;
|
|
}
|
|
|
|
return QObject::eventFilter(object, event);
|
|
}
|
|
|
|
|
|
// Pay the hush URI by showing a confirmation window. If the URI parameter is empty, the UI
|
|
// will prompt for one. If the myAddr is empty, then the default from address is used to send
|
|
// the transaction.
|
|
void MainWindow::payhushURI(QString uri, QString myAddr) {
|
|
qDebug() << __func__ << ": uri=" << uri << " myAddr=" << myAddr;
|
|
// If the Payments UI is not ready (i.e, all balances have not loaded), defer the payment URI
|
|
if (!isPaymentsReady()) {
|
|
qDebug() << "Payment UI not ready, waiting for UI to pay URI";
|
|
pendingURIPayment = uri;
|
|
return;
|
|
}
|
|
|
|
// If there was no URI passed, ask the user for one.
|
|
if (uri.isEmpty()) {
|
|
uri = QInputDialog::getText(this, tr("Paste HUSH URI"),
|
|
"HUSH URI" + QString(" ").repeated(180));
|
|
}
|
|
|
|
// If there's no URI, just exit
|
|
if (uri.isEmpty())
|
|
return;
|
|
|
|
// Extract the address
|
|
qDebug() << "Received URI " << uri;
|
|
PaymentURI paymentInfo = Settings::parseURI(uri);
|
|
if (!paymentInfo.error.isEmpty()) {
|
|
QMessageBox::critical(this, tr("Error paying HUSH URI"),
|
|
tr("URI should be of the form 'hush:<addr>?amt=x&memo=y") + "\n" + paymentInfo.error);
|
|
return;
|
|
}
|
|
|
|
// Now, set the fields on the send tab
|
|
clearSendForm();
|
|
|
|
ui->Address1->setText(paymentInfo.addr);
|
|
ui->Address1->setCursorPosition(0);
|
|
ui->Amount1->setText(paymentInfo.amt);
|
|
ui->MemoTxt1->setText(paymentInfo.memo);
|
|
|
|
// And switch to the send tab.
|
|
ui->tabWidget->setCurrentIndex(1);
|
|
raise();
|
|
|
|
// And click the send button if the amount is > 0, to validate everything. If everything is OK, it will show the confirm box
|
|
// else, show the error message;
|
|
if (paymentInfo.amt > 0) {
|
|
sendButton();
|
|
}
|
|
}
|
|
|
|
void MainWindow::importPrivKey() {
|
|
QDialog d(this);
|
|
Ui_PrivKey pui;
|
|
pui.setupUi(&d);
|
|
Settings::saveRestore(&d);
|
|
|
|
pui.buttonBox->button(QDialogButtonBox::Save)->setVisible(true);
|
|
pui.helpLbl->setText(QString() %
|
|
tr("Please paste your private key(zs-Addr or R-addr) here, one per import") % ".\n" %
|
|
tr("Caution: If this key is for a zaddr it will be NOT included in your Seed. Please send them direct to a Seed zaddr") % ".\n" %
|
|
tr("Transparent address (R.. or t1..) keys will be automatically sent to a zaddr in your Seed")
|
|
);
|
|
|
|
if (d.exec() == QDialog::Accepted && !pui.privKeyTxt->toPlainText().trimmed().isEmpty()) {
|
|
auto rawkeys = pui.privKeyTxt->toPlainText().trimmed().split("\n");
|
|
|
|
QList<QString> keysTmp;
|
|
// Filter out all the empty keys.
|
|
std::copy_if(rawkeys.begin(), rawkeys.end(), std::back_inserter(keysTmp), [=] (auto key) {
|
|
return !key.startsWith("#") && !key.trimmed().isEmpty();
|
|
});
|
|
|
|
auto keys = new QList<QString>();
|
|
std::transform(keysTmp.begin(), keysTmp.end(), std::back_inserter(*keys), [=](auto key) {
|
|
return key.trimmed().split(" ")[0];
|
|
});
|
|
|
|
// Special case.
|
|
// Sometimes, when importing from a paperwallet or such, the key is split by newlines, and might have
|
|
// been pasted like that. So check to see if the whole thing is one big private key
|
|
if (Settings::getInstance()->isValidSaplingPrivateKey(keys->join(""))) {
|
|
auto multiline = keys;
|
|
keys = new QList<QString>();
|
|
keys->append(multiline->join(""));
|
|
delete multiline;
|
|
}
|
|
|
|
// Start the import. The function takes ownership of keys
|
|
QTimer::singleShot(1, [=]() {doImport(keys);});
|
|
|
|
/////Rescan the Wallet (optional) and do automaticly shielding to a seed zaddr
|
|
|
|
if((pui.rescan->isChecked() == true) && (pui.privKeyTxt->toPlainText().startsWith("U"))) {
|
|
|
|
// Show the dialog that keys will be imported and rescan is in progress.
|
|
|
|
QMessageBox::information(this,
|
|
"Imported", tr("The keys were imported. It may take several minutes to rescan the blockchain. Until then, functionality may be limited"),
|
|
QMessageBox::Ok);
|
|
this->getRPC()->clearWallet([=] (auto) {
|
|
// Save the wallet
|
|
this->getRPC()->saveWallet([=] (auto) {
|
|
// Then reload the connection. The ConnectionLoader deletes itself.
|
|
auto cl = new ConnectionLoader(this, rpc);
|
|
cl->loadProgress();
|
|
});
|
|
});
|
|
|
|
}else if ((pui.rescan->isChecked() == true) && (pui.privKeyTxt->toPlainText().startsWith("secret"))){
|
|
|
|
// Show the dialog that keys will be imported.
|
|
|
|
QMessageBox::information(this,
|
|
"Imported", tr("The keys were imported. It may take several minutes to rescan the blockchain. Until then, functionality may be limited"),
|
|
QMessageBox::Ok);
|
|
this->getRPC()->clearWallet([=] (auto) {
|
|
// Save the wallet
|
|
this->getRPC()->saveWallet([=] (auto) {
|
|
// Then reload the connection. The ConnectionLoader deletes itself.
|
|
auto cl = new ConnectionLoader(this, rpc);
|
|
cl->loadConnection();
|
|
});
|
|
});
|
|
|
|
}else{
|
|
QMessageBox::information(this,
|
|
"Imported", tr("The keys were imported without rescan option. The Address you imported will be visible without balance"),
|
|
QMessageBox::Ok);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export transaction history into a CSV file
|
|
*/
|
|
void MainWindow::exportTransactions() {
|
|
// First, get the export file name
|
|
QString exportName = "hush-transactions-" + QDateTime::currentDateTime().toString("yyyyMMdd") + ".csv";
|
|
|
|
QUrl csvName = QFileDialog::getSaveFileUrl(this,
|
|
tr("Export transactions"), exportName, "CSV file (*.csv)");
|
|
|
|
if (csvName.isEmpty())
|
|
return;
|
|
|
|
if (!rpc->getTransactionsModel()->exportToCsv(csvName.toLocalFile())) {
|
|
QMessageBox::critical(this, tr("Error"),
|
|
tr("Error exporting transactions, file was not saved"), QMessageBox::Ok);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export the seed phrase.
|
|
*/
|
|
void MainWindow::exportSeed() {
|
|
if (!rpc->getConnection())
|
|
return;
|
|
|
|
|
|
|
|
rpc->fetchSeed([=](json reply) {
|
|
if (isJsonError(reply)) {
|
|
return;
|
|
}
|
|
|
|
QDialog d(this);
|
|
Ui_PrivKey pui;
|
|
pui.setupUi(&d);
|
|
|
|
// Make the window big by default
|
|
auto ps = this->geometry();
|
|
QMargins margin = QMargins() + 50;
|
|
d.setGeometry(ps.marginsRemoved(margin));
|
|
|
|
Settings::saveRestore(&d);
|
|
|
|
pui.privKeyTxt->setReadOnly(true);
|
|
pui.privKeyTxt->setLineWrapMode(QPlainTextEdit::LineWrapMode::NoWrap);
|
|
pui.privKeyTxt->setPlainText(QString::fromStdString(reply.dump()));
|
|
|
|
pui.helpLbl->setText(tr("This is your wallet seed. Please back it up carefully and safely."));
|
|
|
|
// Wire up save button
|
|
QObject::connect(pui.buttonBox->button(QDialogButtonBox::Save), &QPushButton::clicked, [=] () {
|
|
QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"),
|
|
"Hush-seed.txt");
|
|
QFile file(fileName);
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
QMessageBox::information(this, tr("Unable to open file"), file.errorString());
|
|
return;
|
|
}
|
|
QTextStream out(&file);
|
|
out << pui.privKeyTxt->toPlainText();
|
|
});
|
|
|
|
pui.buttonBox->button(QDialogButtonBox::Save)->setEnabled(true);
|
|
|
|
d.exec();
|
|
});
|
|
}
|
|
|
|
void MainWindow::addPubkey(QString requestZaddr, QString pubkey)
|
|
{
|
|
this->pubkeyMap[requestZaddr] = pubkey;
|
|
}
|
|
|
|
QString MainWindow::getPubkeyByAddress(QString requestZaddr)
|
|
{
|
|
if(this->pubkeyMap.count(requestZaddr) > 0)
|
|
{
|
|
return this->pubkeyMap[requestZaddr];
|
|
}
|
|
|
|
return QString("0xdeadbeef");
|
|
}
|
|
|
|
|
|
void MainWindow::exportAllKeys() {
|
|
exportKeys("");
|
|
}
|
|
|
|
void MainWindow::exportKeys(QString addr) {
|
|
if (!rpc->getConnection())
|
|
return;
|
|
|
|
bool allKeys = addr.isEmpty() ? true : false;
|
|
|
|
auto fnUpdateUIWithKeys = [=](json reply) {
|
|
if (isJsonError(reply)) {
|
|
return;
|
|
}
|
|
|
|
if (reply.is_discarded() || !reply.is_array()) {
|
|
QMessageBox::critical(this, tr("Error getting private keys"),
|
|
tr("Error loading private keys: ") + QString::fromStdString(reply.dump()),
|
|
QMessageBox::Ok);
|
|
return;
|
|
}
|
|
|
|
QDialog d(this);
|
|
Ui_PrivKey pui;
|
|
pui.setupUi(&d);
|
|
|
|
// Make the window big by default
|
|
auto ps = this->geometry();
|
|
QMargins margin = QMargins() + 50;
|
|
d.setGeometry(ps.marginsRemoved(margin));
|
|
|
|
Settings::saveRestore(&d);
|
|
|
|
pui.privKeyTxt->setReadOnly(true);
|
|
pui.privKeyTxt->setLineWrapMode(QPlainTextEdit::LineWrapMode::NoWrap);
|
|
|
|
if (allKeys)
|
|
pui.helpLbl->setText(tr("These are all the private keys for all the addresses in your wallet"));
|
|
else
|
|
pui.helpLbl->setText(tr("Private key for ") + addr);
|
|
|
|
|
|
// Wire up save button
|
|
QObject::connect(pui.buttonBox->button(QDialogButtonBox::Save), &QPushButton::clicked, [=] () {
|
|
QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"),
|
|
allKeys ? "Hush-all-privatekeys.txt" : "Hush-privatekey.txt");
|
|
QFile file(fileName);
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
QMessageBox::information(this, tr("Unable to open file"), file.errorString());
|
|
return;
|
|
}
|
|
QTextStream out(&file);
|
|
out << pui.privKeyTxt->toPlainText();
|
|
});
|
|
|
|
QString allKeysTxt;
|
|
for (auto i : reply.get<json::array_t>()) {
|
|
allKeysTxt = allKeysTxt % QString::fromStdString(i["private_key"]) % " # addr=" % QString::fromStdString(i["address"]) % "\n";
|
|
}
|
|
|
|
pui.privKeyTxt->setPlainText(allKeysTxt);
|
|
pui.buttonBox->button(QDialogButtonBox::Save)->setEnabled(true);
|
|
|
|
d.exec();
|
|
};
|
|
|
|
if (allKeys) {
|
|
rpc->fetchAllPrivKeys(fnUpdateUIWithKeys);
|
|
}
|
|
else {
|
|
rpc->fetchPrivKey(addr, fnUpdateUIWithKeys);
|
|
}
|
|
}
|
|
|
|
void MainWindow::setupBalancesTab() {
|
|
ui->unconfirmedWarning->setVisible(false);
|
|
ui->lblSyncWarning->setVisible(false);
|
|
ui->lblSyncWarningReceive->setVisible(false);
|
|
QObject::connect(ui->depositHushButton, &QPushButton::clicked, [=](){
|
|
|
|
Ui_deposithush deposithush;
|
|
QDialog dialog(this);
|
|
deposithush.setupUi(&dialog);
|
|
Settings::saveRestore(&dialog);
|
|
|
|
QList<QString> allAddresses;
|
|
|
|
allAddresses = getRPC()->getModel()->getAllZAddresses();
|
|
QString depositzaddr = allAddresses[0];
|
|
deposithush.qrcodeDisplayDeposit->setQrcodeString(depositzaddr);
|
|
deposithush.zaddr->setText(depositzaddr);
|
|
|
|
QObject::connect(deposithush.CopyAddress, &QPushButton::clicked, [=](){
|
|
|
|
QGuiApplication::clipboard()->setText(depositzaddr);
|
|
ui->statusBar->showMessage(tr("Copied to clipboard"), 3 * 1000);
|
|
|
|
});
|
|
|
|
|
|
|
|
dialog.exec();
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
// Setup context menu on balances tab
|
|
ui->balancesTable->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
QObject::connect(ui->balancesTable, &QTableView::customContextMenuRequested, [=] (QPoint pos) {
|
|
QModelIndex index = ui->balancesTable->indexAt(pos);
|
|
if (index.row() < 0) return;
|
|
|
|
index = index.sibling(index.row(), 0);
|
|
auto addr = AddressBook::addressFromAddressLabel(
|
|
ui->balancesTable->model()->data(index).toString());
|
|
|
|
QMenu menu(this);
|
|
|
|
menu.addAction(tr("Copy address"), [=] () {
|
|
QClipboard *clipboard = QGuiApplication::clipboard();
|
|
clipboard->setText(addr);
|
|
ui->statusBar->showMessage(tr("Copied to clipboard"), 3 * 1000);
|
|
});
|
|
|
|
menu.addAction(tr("Get private key"), [=] () {
|
|
this->exportKeys(addr);
|
|
});
|
|
|
|
|
|
if (Settings::isTAddress(addr)) {
|
|
menu.addAction(tr("View on block explorer"), [=] () {
|
|
Settings::openAddressInExplorer(addr);
|
|
});
|
|
}
|
|
|
|
menu.exec(ui->balancesTable->viewport()->mapToGlobal(pos));
|
|
});
|
|
|
|
}
|
|
|
|
void MainWindow::setuphushdTab() {
|
|
ui->hushdlogo->setBasePixmap(QPixmap(":/img/res/hushdlogo.png"));
|
|
}
|
|
|
|
void MainWindow::setupTransactionsTab() {
|
|
// Double click opens up memo if one exists
|
|
QObject::connect(ui->transactionsTable, &QTableView::doubleClicked, [=] (auto index) {
|
|
auto txModel = dynamic_cast<TxTableModel *>(ui->transactionsTable->model());
|
|
QString memo = txModel->getMemo(index.row());
|
|
|
|
if (!memo.isEmpty()) {
|
|
QMessageBox mb(QMessageBox::Information, tr("Memo"), memo, QMessageBox::Ok, this);
|
|
mb.setTextFormat(Qt::PlainText);
|
|
mb.setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
|
|
mb.exec();
|
|
}
|
|
});
|
|
|
|
// Set up context menu on transactions tab
|
|
ui->listChat->setContextMenuPolicy(Qt::ActionsContextMenu);
|
|
contextMenuChat->setObjectName("contextMenuChat");
|
|
QString style = "QMenu{background-color: rgb(0, 0, 255);}";
|
|
auto theme = Settings::getInstance()->get_theme_name();
|
|
if (theme == "Dark" || theme == "Midnight") {
|
|
ui->listChat->setStyleSheet("QListView{background-image: url(:/icons/res/SDLogo.png) ;background-position: center center ;background-repeat: no-repeat;} QMenu::item:selected { border-color: darkblue; background: rgba(100, 100, 100, 150);}");}
|
|
if (theme == "Default") {ui->listChat->setStyleSheet("QListView{background-image: url(:/icons/res/sdlogo2.png) ;background-attachment: fixed ;background-position: center center ;background-repeat: no-repeat;} QMenu::item:selected { border-color: darkblue; background: rgba(100, 100, 100, 150);}");}
|
|
ui->listChat->setResizeMode(QListView::Adjust);
|
|
ui->listChat->setWordWrap(true);
|
|
ui->listChat->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
ui->listChat->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
ui->listChat->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
ui->listChat->setMinimumSize(200,350);
|
|
ui->listChat->setItemDelegate(new ListViewDelegate());
|
|
ui->listChat->show();
|
|
|
|
ui->transactionsTable->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
// Table right click
|
|
QObject::connect(ui->transactionsTable, &QTableView::customContextMenuRequested, [=] (QPoint pos) {
|
|
QModelIndex index = ui->transactionsTable->indexAt(pos);
|
|
if (index.row() < 0) return;
|
|
|
|
QMenu menu(this);
|
|
|
|
auto txModel = dynamic_cast<TxTableModel *>(ui->transactionsTable->model());
|
|
|
|
QString txid = txModel->getTxId(index.row());
|
|
QString memo = txModel->getMemo(index.row());
|
|
QString addr = txModel->getAddr(index.row());
|
|
|
|
menu.addAction(tr("Copy txid"), [=] () {
|
|
QGuiApplication::clipboard()->setText(txid);
|
|
ui->statusBar->showMessage(tr("Copied to clipboard"), 3 * 1000);
|
|
});
|
|
|
|
if (!addr.isEmpty()) {
|
|
menu.addAction(tr("Copy address"), [=] () {
|
|
QGuiApplication::clipboard()->setText(addr);
|
|
ui->statusBar->showMessage(tr("Copied to clipboard"), 3 * 1000);
|
|
});
|
|
}
|
|
menu.addAction(tr("Copy block explorer link"), [=]() {
|
|
// auto explorer = Settings::getInstance()->getExplorer();
|
|
QGuiApplication::clipboard()->setText("https://explorer.hush.is/tx/" + txid);
|
|
});
|
|
|
|
menu.addAction(tr("View on block explorer"), [=] () {
|
|
Settings::openTxInExplorer(txid);
|
|
});
|
|
|
|
// Payment Request
|
|
if (!memo.isEmpty() && memo.startsWith("hush:")) {
|
|
menu.addAction(tr("View Payment Request"), [=] () {
|
|
RequestDialog::showPaymentConfirmation(this, memo);
|
|
});
|
|
}
|
|
|
|
// View Memo
|
|
if (!memo.isEmpty()) {
|
|
menu.addAction(tr("View Memo"), [=] () {
|
|
QMessageBox mb(QMessageBox::Information, tr("Memo"), memo, QMessageBox::Ok, this);
|
|
mb.setTextFormat(Qt::PlainText);
|
|
mb.setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
|
|
mb.exec();
|
|
});
|
|
}
|
|
|
|
// If memo contains a reply to address, add a "Reply to" menu item
|
|
if (!memo.isEmpty()) {
|
|
int lastPost = memo.trimmed().lastIndexOf(QRegExp("[\r\n]+"));
|
|
QString lastWord = memo.right(memo.length() - lastPost - 1);
|
|
|
|
if (Settings::getInstance()->isSaplingAddress(lastWord)) {
|
|
menu.addAction(tr("Reply to ") + lastWord.left(25) + "...", [=]() {
|
|
// First, cancel any pending stuff in the send tab by pretending to click
|
|
// the cancel button
|
|
cancelButton();
|
|
|
|
// Then set up the fields in the send tab
|
|
ui->Address1->setText(lastWord);
|
|
ui->Address1->setCursorPosition(0);
|
|
ui->Amount1->setText("0.0001");
|
|
|
|
// And switch to the send tab.
|
|
ui->tabWidget->setCurrentIndex(1);
|
|
|
|
qApp->processEvents();
|
|
|
|
// Click the memo button
|
|
this->memoButtonClicked(1, true);
|
|
});
|
|
}
|
|
}
|
|
|
|
menu.exec(ui->transactionsTable->viewport()->mapToGlobal(pos));
|
|
});
|
|
|
|
}
|
|
|
|
void MainWindow::setupchatTab() {
|
|
|
|
ui->memoTxtChat->setEnabled(false);
|
|
ui->emojiButton->setEnabled(false);
|
|
ui->sendChatButton->setEnabled(false);
|
|
|
|
/////////////Setting Icons for Chattab and different themes
|
|
|
|
auto theme = Settings::getInstance()->get_theme_name();
|
|
if (theme == "Dark" || theme == "Midnight") {
|
|
QPixmap send(":/icons/res/send-white.png");
|
|
QIcon sendIcon(send);
|
|
ui->sendChatButton->setIcon(sendIcon);
|
|
|
|
QPixmap notification(":/icons/res/requestWhite.png");
|
|
QIcon notificationIcon(notification);
|
|
ui->pushContact->setIcon(notificationIcon);
|
|
|
|
QPixmap addContact(":/icons/res/addContactWhite.png");
|
|
QIcon addContactIcon(addContact);
|
|
ui->safeContactRequest->setIcon(addContactIcon);
|
|
|
|
QPixmap newAddr(":/icons/res/getAddrWhite.png");
|
|
QIcon addnewAddrIcon(newAddr);
|
|
ui->givemeZaddr->setIcon(addnewAddrIcon);
|
|
|
|
ui->memoTxtChat->setTextColor("White");
|
|
|
|
}else{
|
|
|
|
QPixmap send(":/icons/res/sendBlack.png");
|
|
QIcon sendIcon(send);
|
|
ui->sendChatButton->setIcon(sendIcon);
|
|
|
|
QPixmap notification(":/icons/res/requestBlack.png");
|
|
QIcon notificationIcon(notification);
|
|
ui->pushContact->setIcon(notificationIcon);
|
|
|
|
QPixmap addContact(":/icons/res/addContactBlack.png");
|
|
QIcon addContactIcon(addContact);
|
|
ui->safeContactRequest->setIcon(addContactIcon);
|
|
|
|
QPixmap newAddr(":/icons/res/getAddrBlack.png");
|
|
QIcon addnewAddrIcon(newAddr);
|
|
ui->givemeZaddr->setIcon(addnewAddrIcon);
|
|
|
|
ui->memoTxtChat->setTextColor("Black");
|
|
}
|
|
|
|
QObject::connect(ui->sendChatButton, &QPushButton::clicked, this, &MainWindow::sendChat);
|
|
QObject::connect(ui->sendChatButton, &QPushButton::clicked, [&] () {
|
|
|
|
ui->memoTxtChat->setEnabled(false);
|
|
ui->emojiButton->setEnabled(false);
|
|
|
|
|
|
});
|
|
QObject::connect(ui->safeContactRequest, &QPushButton::clicked, this, &MainWindow::addContact);
|
|
QObject::connect(ui->pushContact, &QPushButton::clicked, this , &MainWindow::renderContactRequest);
|
|
|
|
ui->contactNameMemo->setText("");
|
|
|
|
/////Setup Actions
|
|
|
|
QAction* copymessage;
|
|
QAction* viewexplorer;
|
|
QAction* copytxid;
|
|
QAction* copylink;
|
|
QAction* openlink;
|
|
|
|
copymessage = new QAction("Copy message to clipboard",contextMenuChat);
|
|
viewexplorer = new QAction("View on block explorer",contextMenuChat);
|
|
copytxid = new QAction("Copy txid to clipboard ",contextMenuChat);
|
|
copylink = new QAction("Copy Hyperlink from memo ",contextMenuChat);
|
|
openlink = new QAction("Open Hyperlink in your browser",contextMenuChat);
|
|
|
|
QObject::connect(ui->listContactWidget, &QTableView::clicked, [=] () {
|
|
|
|
ui->listChat->addAction(copymessage);
|
|
ui->listChat->addAction(viewexplorer);
|
|
ui->listChat->addAction(copytxid);
|
|
ui->listChat->addAction(copylink);
|
|
ui->listChat->addAction(openlink);
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(copylink, &QAction::triggered, [=] {
|
|
|
|
QModelIndex index = ui->listChat->currentIndex();
|
|
QString memo_chat = index.data(Qt::DisplayRole).toString();
|
|
QClipboard *clipboard = QGuiApplication::clipboard();
|
|
QRegExp rx("((?:https?|ftp)://\\S+)");
|
|
int pos = rx.indexIn(memo_chat, 0);
|
|
if (-1 != pos)
|
|
{
|
|
QString cap = rx.cap(0);
|
|
cap = cap.left(cap.indexOf('\''));
|
|
int startPos = cap.indexOf("<p>");
|
|
int endPos = cap.indexOf("</p>");
|
|
int length = endPos - startPos;
|
|
QString hyperlink = cap.mid(startPos, length);
|
|
clipboard->setText(hyperlink);
|
|
ui->statusBar->showMessage(tr("Copied Hyperlink to clipboard"), 3 * 1000);
|
|
|
|
}
|
|
});
|
|
|
|
QObject::connect(openlink, &QAction::triggered, [=] {
|
|
|
|
QModelIndex index = ui->listChat->currentIndex();
|
|
QString memo_chat = index.data(Qt::DisplayRole).toString();
|
|
QRegExp rx("((?:https?|ftp)://\\S+)");
|
|
int pos = rx.indexIn(memo_chat, 0);
|
|
if (-1 != pos)
|
|
{
|
|
QString cap = rx.cap(0);
|
|
cap = cap.left(cap.indexOf('\''));
|
|
int startPos = cap.indexOf("<p>");
|
|
int endPos = cap.indexOf("</p>");
|
|
int length = endPos - startPos;
|
|
QString hyperlink = cap.mid(startPos, length);
|
|
QDesktopServices::openUrl(QUrl(hyperlink));
|
|
|
|
}
|
|
});
|
|
|
|
QObject::connect(copymessage, &QAction::triggered, [=] {
|
|
|
|
|
|
QModelIndex index = ui->listChat->currentIndex();
|
|
QString memo_chat = index.data(Qt::DisplayRole).toString();
|
|
QClipboard *clipboard = QGuiApplication::clipboard();
|
|
int startPos = memo_chat.indexOf("<p>") + 3;
|
|
int endPos = memo_chat.indexOf("</p>");
|
|
int length = endPos - startPos;
|
|
QString copymemo = memo_chat.mid(startPos, length);
|
|
|
|
clipboard->setText(copymemo);
|
|
ui->statusBar->showMessage(tr("Copied message to clipboard"), 3 * 1000);
|
|
|
|
});
|
|
QObject::connect(copytxid, &QAction::triggered, [=] {
|
|
|
|
QModelIndex index = ui->listChat->currentIndex();
|
|
QString memo_chat = index.data(Qt::DisplayRole).toString();
|
|
QClipboard *clipboard = QGuiApplication::clipboard();
|
|
|
|
int startPos = memo_chat.indexOf("<p>") + 3;
|
|
int endPos = memo_chat.indexOf("</p>");
|
|
int length = endPos - startPos;
|
|
QString copymemo = memo_chat.mid(startPos, length);
|
|
int startPosT = memo_chat.indexOf("<small>") + 7;
|
|
int endPosT = memo_chat.indexOf("<b>");
|
|
int lengthT = endPosT - startPosT;
|
|
|
|
QString time = memo_chat.mid(startPosT, lengthT);
|
|
|
|
for (auto &c : DataStore::getChatDataStore()->getAllRawChatItems()){
|
|
|
|
if (c.second.getMemo() == copymemo)
|
|
{
|
|
int timestamp = c.second.getTimestamp();
|
|
QDateTime myDateTime;
|
|
QString lock;
|
|
myDateTime.setTime_t(timestamp);
|
|
QString timestamphtml = myDateTime.toString("yyyy-MM-dd hh:mm");
|
|
|
|
if(timestamphtml == time)
|
|
|
|
{
|
|
clipboard->setText(c.second.getTxid());
|
|
ui->statusBar->showMessage(tr("Copied Txid to clipboard"), 3 * 1000);
|
|
}else{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
QObject::connect(viewexplorer, &QAction::triggered, [=] {
|
|
|
|
QModelIndex index = ui->listChat->currentIndex();
|
|
QString memo_chat = index.data(Qt::DisplayRole).toString();
|
|
|
|
int startPos = memo_chat.indexOf("<p>") + 3;
|
|
int endPos = memo_chat.indexOf("</p>");
|
|
int length = endPos - startPos;
|
|
QString copymemo = memo_chat.mid(startPos, length);
|
|
int startPosT = memo_chat.indexOf("<small>") + 7;
|
|
int endPosT = memo_chat.indexOf("<b>");
|
|
int lengthT = endPosT - startPosT;
|
|
|
|
QString time = memo_chat.mid(startPosT, lengthT);
|
|
|
|
for (auto &c : DataStore::getChatDataStore()->getAllRawChatItems()){
|
|
|
|
if (c.second.getMemo() == copymemo)
|
|
{
|
|
int timestamp = c.second.getTimestamp();
|
|
QDateTime myDateTime;
|
|
QString lock;
|
|
myDateTime.setTime_t(timestamp);
|
|
QString timestamphtml = myDateTime.toString("yyyy-MM-dd hh:mm");
|
|
|
|
if(timestamphtml == time)
|
|
|
|
{
|
|
|
|
Settings::openTxInExplorer(c.second.getTxid());
|
|
|
|
}else{}
|
|
|
|
}
|
|
|
|
}
|
|
});
|
|
|
|
|
|
|
|
///////// Add contextmenu
|
|
QMenu* contextMenu;
|
|
QAction* editAction;
|
|
QAction* HushAction;
|
|
contextMenu = new QMenu(ui->listContactWidget);
|
|
HushAction = new QAction("Send or Request Hush ",contextMenu);
|
|
editAction = new QAction("Delete this contact",contextMenu);
|
|
|
|
///////// Set selected Zaddr for Chat with click
|
|
|
|
QObject::connect(ui->listContactWidget, &QTableView::clicked, [=] () {
|
|
|
|
ui->listContactWidget->setContextMenuPolicy(Qt::ActionsContextMenu);
|
|
ui->listContactWidget->addAction(HushAction);
|
|
ui->listContactWidget->addAction(editAction);
|
|
int requestsize = DataStore::getChatDataStore()->getAllNewContactRequests().size();
|
|
|
|
if (requestsize > 0)
|
|
{
|
|
ui->newRequest->setStyleSheet("color: red;");
|
|
}else{}
|
|
|
|
ui->newRequest->setNum(requestsize);
|
|
|
|
ui->memoTxtChat->setEnabled(false);
|
|
ui->emojiButton->setEnabled(false);
|
|
ui->sendChatButton->setEnabled(false);
|
|
|
|
QModelIndex index = ui->listContactWidget->currentIndex();
|
|
QString label_contact = index.data(Qt::DisplayRole).toString();
|
|
|
|
for(auto &p : AddressBook::getInstance()->getAllAddressLabels())
|
|
if (label_contact == p.getName()) {
|
|
ui->contactNameMemo->setText(p.getName());
|
|
rpc->refresh(true);
|
|
|
|
}
|
|
});
|
|
|
|
QObject::connect(HushAction, &QAction::triggered, [=]() {
|
|
|
|
QModelIndex index = ui->listContactWidget->currentIndex();
|
|
QString label_contact = index.data(Qt::DisplayRole).toString();
|
|
|
|
Ui_transactionHush transaction;
|
|
QDialog transactionDialog(this);
|
|
transaction.setupUi(&transactionDialog);
|
|
Settings::saveRestore(&transactionDialog);
|
|
transaction.amountChat->setValidator(this->getAmountValidator());
|
|
QString icon = ":icons/res/hush-money-white.png";
|
|
QPixmap hush(icon);
|
|
transaction.label_3->setPixmap(hush);
|
|
|
|
|
|
|
|
for(auto &p : AddressBook::getInstance()->getAllAddressLabels())
|
|
if (label_contact == p.getName()) {
|
|
|
|
QStandardItemModel* contact = new QStandardItemModel();
|
|
QString avatar = p.getAvatar();
|
|
QStandardItem* Items1 = new QStandardItem(p.getName());
|
|
Items1->setData(QIcon(avatar),Qt::DecorationRole);
|
|
contact->appendRow(Items1);
|
|
transaction.contactName->setModel(contact);
|
|
transaction.contactName->setIconSize(QSize(60,70));
|
|
transaction.contactName->setUniformItemSizes(true);
|
|
transaction.contactName->setDragDropMode(QAbstractItemView::DropOnly);
|
|
transaction.contactName->show();
|
|
|
|
|
|
}
|
|
|
|
QObject::connect(transaction.sendHush, &QPushButton::clicked, [&] (){
|
|
|
|
QString amt = transaction.amountChat->text();
|
|
QString memo = transaction.MemoMoney->text();
|
|
this->setAmt(amt);
|
|
this->setMoneyMemo(memo);
|
|
transactionDialog.close();
|
|
});
|
|
|
|
QObject::connect(transaction.sendHush, &QPushButton::clicked, this , &MainWindow::sendMoneyChat);
|
|
|
|
|
|
|
|
QObject::connect(transaction.requestHush, &QPushButton::clicked, [&] (){
|
|
|
|
QString amt = transaction.amountChat->text();
|
|
QString memo = transaction.MemoMoney->text();
|
|
this->setAmt(amt);
|
|
this->setMoneyMemo(memo);
|
|
transactionDialog.close();
|
|
});
|
|
|
|
QObject::connect(transaction.requestHush, &QPushButton::clicked, this , &MainWindow::sendMoneyRequestChat);
|
|
|
|
|
|
|
|
transactionDialog.exec();
|
|
|
|
});
|
|
|
|
QObject::connect(editAction, &QAction::triggered, [=]() {
|
|
QModelIndex index = ui->listContactWidget->currentIndex();
|
|
QString label_contact = index.data(Qt::DisplayRole).toString();
|
|
|
|
for(auto &p : AddressBook::getInstance()->getAllAddressLabels())
|
|
if (label_contact == p.getName()) {
|
|
|
|
QString label1 = p.getName();
|
|
QString addr = p.getPartnerAddress();
|
|
QString myzaddr = p.getMyAddress();
|
|
QString cid = p.getCid();
|
|
QString avatar = p.getAvatar();
|
|
|
|
|
|
AddressBook::getInstance()->removeAddressLabel(label1, addr, myzaddr, cid,avatar);
|
|
rpc->refreshContacts(
|
|
ui->listContactWidget);
|
|
rpc->refresh(true);
|
|
}
|
|
});
|
|
|
|
|
|
|
|
ui->memoTxtChat->setLenDisplayLabelChat(ui->memoSizeChat);
|
|
ui->newRequest->setStyleSheet("color: red;");
|
|
|
|
|
|
////get amount of new contact request as intervall
|
|
|
|
QTimer* timer = new QTimer();
|
|
timer->setInterval(30000);
|
|
|
|
connect(timer, &QTimer::timeout, this, [=](){
|
|
int requestsize = DataStore::getChatDataStore()->getAllNewContactRequests().size();
|
|
ui->newRequest->setNum(requestsize);
|
|
});
|
|
timer->start();
|
|
|
|
}
|
|
|
|
// Create a Tx from the current state of the Chat page.
|
|
Tx MainWindow::createTxFromSendChatPage() {
|
|
Tx tx;
|
|
CAmount totalAmt;
|
|
// For each addr/amt in the Chat tab
|
|
{
|
|
|
|
QString amtStr = this->getAmt();
|
|
CAmount amt;
|
|
CAmount amtHm;
|
|
|
|
amt = CAmount::fromDecimalString(amtStr);
|
|
amtHm = CAmount::fromDecimalString("0");
|
|
totalAmt = totalAmt + amt;
|
|
|
|
QModelIndex index = ui->listContactWidget->currentIndex();
|
|
QString label_contact = index.data(Qt::DisplayRole).toString();
|
|
|
|
for(auto &c : AddressBook::getInstance()->getAllAddressLabels())
|
|
|
|
if (label_contact == c.getName()) {
|
|
|
|
QString cid = c.getCid();
|
|
QString myAddr = c.getMyAddress();
|
|
QString type = "Money";
|
|
QString addr = c.getPartnerAddress();
|
|
QString moneymemo = this->getMoneyMemo();
|
|
|
|
/////////User input for chatmemos
|
|
QString memoplain = QString("Money transaction of : ") + amtStr + QString(" HUSH") + QString("\n") + QString("\n") + moneymemo;
|
|
|
|
/////////We convert the user input from QString to unsigned char*, so we can encrypt it later
|
|
int lengthmemo = memoplain.length();
|
|
|
|
char *memoplainchar = NULL;
|
|
memoplainchar = new char[lengthmemo+2];
|
|
strncpy(memoplainchar, memoplain.toUtf8(), lengthmemo +1);
|
|
|
|
QString pubkey = this->getPubkeyByAddress(addr);
|
|
QString passphraseHash = DataStore::getChatDataStore()->getPassword();
|
|
int length = passphraseHash.length();
|
|
|
|
////////////////Generate the secretkey for our message encryption
|
|
|
|
char *hashEncryptionKeyraw = NULL;
|
|
hashEncryptionKeyraw = new char[length+1];
|
|
strncpy(hashEncryptionKeyraw, passphraseHash.toUtf8(), length+1);
|
|
|
|
#define MESSAGEAS1 ((const unsigned char *) hashEncryptionKeyraw)
|
|
#define MESSAGEAS1_LEN length
|
|
|
|
|
|
unsigned char sk[crypto_kx_SECRETKEYBYTES];
|
|
unsigned char pk[crypto_kx_PUBLICKEYBYTES];
|
|
unsigned char server_rx[crypto_kx_SESSIONKEYBYTES], server_tx[crypto_kx_SESSIONKEYBYTES];
|
|
|
|
if (crypto_kx_seed_keypair(pk,sk, MESSAGEAS1) !=0) {
|
|
this->logger->write("Suspicious keypair, bail out ");
|
|
qDebug() << __func__ << ": Suspicious client public send key from crypto_kx_seed_keypair, aborting!";
|
|
return tx;
|
|
}
|
|
////////////////Get the pubkey from Bob, so we can create the share key
|
|
|
|
const QByteArray pubkeyBobArray = QByteArray::fromHex(pubkey.toLatin1());
|
|
const unsigned char *pubkeyBob = reinterpret_cast<const unsigned char *>(pubkeyBobArray.constData());
|
|
/////Create the shared key for sending the message
|
|
|
|
if (crypto_kx_server_session_keys(server_rx, server_tx, pk, sk, pubkeyBob) != 0) {
|
|
this->logger->write("Suspicious client public send key, bail out ");
|
|
qDebug() << __func__ << ": Suspicious client public send key from crypto_kx_server_session_keys, aborting!";
|
|
return tx;
|
|
}
|
|
|
|
|
|
// Let's try to preserve Unicode characters
|
|
QByteArray ba_memo = memoplain.toUtf8();
|
|
int ba_memo_length = ba_memo.size();
|
|
|
|
#define MESSAGEMoney (const unsigned char *) ba_memo.data()
|
|
#define MESSAGE_LENMoney ba_memo_length
|
|
|
|
|
|
////////////Now lets encrypt the message Alice send to Bob//////////////////////////////
|
|
//#define MESSAGE (const unsigned char *) memoplainchar
|
|
//#define MESSAGE_LEN lengthmemo
|
|
#define CIPHERTEXT_LEN (crypto_secretstream_xchacha20poly1305_ABYTES + MESSAGE_LENMoney)
|
|
unsigned char ciphertext[CIPHERTEXT_LEN];
|
|
unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];
|
|
|
|
crypto_secretstream_xchacha20poly1305_state state;
|
|
|
|
/* Set up a new stream: initialize the state and create the header */
|
|
crypto_secretstream_xchacha20poly1305_init_push(&state, header, server_tx);
|
|
|
|
|
|
/* Now, encrypt the first chunk. `c1` will contain an encrypted,
|
|
* authenticated representation of `MESSAGE_PART1`. */
|
|
crypto_secretstream_xchacha20poly1305_push
|
|
(&state, ciphertext, NULL, MESSAGEMoney, MESSAGE_LENMoney, NULL, 0, crypto_secretstream_xchacha20poly1305_TAG_FINAL);
|
|
|
|
////Create the HM for this message
|
|
QString headerbytes = QByteArray(reinterpret_cast<const char*>(header), crypto_secretstream_xchacha20poly1305_HEADERBYTES).toHex();
|
|
QString publickeyAlice = QByteArray(reinterpret_cast<const char*>(pk), crypto_kx_PUBLICKEYBYTES).toHex();
|
|
|
|
|
|
QString hmemo= createHeaderMemo(type,cid,myAddr,headerbytes,publickeyAlice,1,0);
|
|
|
|
/////Ciphertext Memo
|
|
QString memo = QByteArray(reinterpret_cast<const char*>(ciphertext), CIPHERTEXT_LEN).toHex();
|
|
|
|
tx.toAddrs.push_back(ToFields{addr, amtHm, hmemo});
|
|
tx.toAddrs.push_back(ToFields{addr, amt, memo});
|
|
}
|
|
}
|
|
|
|
tx.fee = Settings::getMinerFee();
|
|
|
|
return tx;
|
|
|
|
}
|
|
|
|
void MainWindow::sendMoneyChat() {
|
|
|
|
////////////////////////////Todo: Check if a Contact is selected//////////
|
|
|
|
// Create a Tx from the values on the send tab. Note that this Tx object
|
|
// might not be valid yet.
|
|
|
|
/* QString Name = ui->contactNameMemo->text();
|
|
|
|
if ((ui->contactNameMemo->text().isEmpty()) || (ui->memoTxtChat->toPlainText().trimmed().isEmpty())) {
|
|
|
|
QMessageBox msg(QMessageBox::Critical, tr("You have to select a contact and insert a Memo"),
|
|
tr("You have selected no Contact from Contactlist,\n") + tr("\nor your Memo is empty"),
|
|
QMessageBox::Ok, this);
|
|
|
|
msg.exec();
|
|
return;
|
|
}*/
|
|
|
|
|
|
Tx tx = createTxFromSendChatPage();
|
|
|
|
QString error = doSendChatMoneyTxValidations(tx);
|
|
|
|
if (!error.isEmpty()) {
|
|
// Something went wrong, so show an error and exit
|
|
QMessageBox msg(QMessageBox::Critical, tr("Message Error"), error,
|
|
QMessageBox::Ok, this);
|
|
|
|
msg.exec();
|
|
ui->memoTxtChat->setEnabled(true);
|
|
|
|
// abort the Tx
|
|
return;
|
|
}
|
|
|
|
auto movie = new QMovie(this);
|
|
auto movie1 = new QMovie(this);
|
|
movie->setFileName(":/img/res/loaderblack.gif");
|
|
movie1->setFileName(":/img/res/loaderwhite.gif");
|
|
|
|
auto theme = Settings::getInstance()->get_theme_name();
|
|
if (theme == "Dark" || theme == "Midnight") {
|
|
|
|
connect(movie, &QMovie::frameChanged, [=]{
|
|
ui->sendChatButton->setIcon(movie->currentPixmap());
|
|
});
|
|
movie->start();
|
|
ui->sendChatButton->show();
|
|
ui->sendChatButton->setEnabled(false);
|
|
|
|
} else {
|
|
|
|
connect(movie1, &QMovie::frameChanged, [=]{
|
|
ui->sendChatButton->setIcon(movie1->currentPixmap());
|
|
});
|
|
movie1->start();
|
|
ui->sendChatButton->show();
|
|
ui->sendChatButton->setEnabled(false);
|
|
ui->memoTxtChat->setEnabled(false);
|
|
}
|
|
|
|
ui->memoTxtChat->clear();
|
|
|
|
// And send the Tx
|
|
rpc->executeTransaction(tx,
|
|
[=] (QString txid) {
|
|
ui->statusBar->showMessage(Settings::txidStatusMessage + " " + txid);
|
|
|
|
|
|
QTimer::singleShot(1000, [=]() {
|
|
|
|
if (theme == "Dark" || theme == "Midnight") {
|
|
QPixmap send(":/icons/res/send-white.png");
|
|
QIcon sendIcon(send);
|
|
ui->sendChatButton->setIcon(sendIcon);
|
|
movie->stop();
|
|
ui->sendChatButton->setEnabled(true);
|
|
ui->memoTxtChat->setEnabled(true);
|
|
}else{
|
|
|
|
QPixmap send(":/icons/res/sendBlack.png");
|
|
QIcon sendIcon(send);
|
|
ui->sendChatButton->setIcon(sendIcon);
|
|
movie1->stop();
|
|
ui->sendChatButton->setEnabled(true);
|
|
ui->memoTxtChat->setEnabled(true);
|
|
}
|
|
|
|
});
|
|
|
|
// Force a UI update so we get the unconfirmed Tx
|
|
rpc->refresh(true);
|
|
ui->memoTxtChat->clear();
|
|
// ui->memoTxtChat->setEnabled(true);
|
|
|
|
},
|
|
// Errored out
|
|
[=] (QString opid, QString errStr) {
|
|
ui->statusBar->showMessage(QObject::tr(" Tx ") % opid % QObject::tr(" failed"), 15 * 1000);
|
|
ui->memoTxtChat->setEnabled(true);
|
|
|
|
if (!opid.isEmpty())
|
|
errStr = QObject::tr("The transaction with id ") % opid % QObject::tr(" failed. The error was") + ":\n\n" + errStr;
|
|
|
|
QMessageBox::critical(this, QObject::tr("Transaction Error"), errStr, QMessageBox::Ok);
|
|
movie->stop();
|
|
ui->memoTxtChat->setEnabled(true);
|
|
|
|
|
|
if (theme == "Dark" || theme == "Midnight") {
|
|
QPixmap send(":/icons/res/send-white.png");
|
|
QIcon sendIcon(send);
|
|
ui->sendChatButton->setIcon(sendIcon);
|
|
movie->stop();
|
|
ui->sendChatButton->setEnabled(true);
|
|
ui->memoTxtChat->setEnabled(true);
|
|
}else{
|
|
|
|
QPixmap send(":/icons/res/sendBlack.png");
|
|
QIcon sendIcon(send);
|
|
ui->sendChatButton->setIcon(sendIcon);
|
|
movie1->stop();
|
|
ui->sendChatButton->setEnabled(true);
|
|
ui->memoTxtChat->setEnabled(true);
|
|
}
|
|
|
|
|
|
}
|
|
);
|
|
|
|
}
|
|
|
|
QString MainWindow::doSendChatMoneyTxValidations(Tx tx) {
|
|
// Check to see if we have enough verified funds to send the Tx.
|
|
|
|
CAmount total;
|
|
for (auto toAddr : tx.toAddrs) {
|
|
if (!Settings::isValidAddress(toAddr.addr)) {
|
|
QString addr = (toAddr.addr.length() > 100 ? toAddr.addr.left(100) + "..." : toAddr.addr);
|
|
return QString(tr("Recipient Address ")) % addr % tr(" is Invalid");
|
|
ui->memoTxtChat->setEnabled(true);
|
|
}
|
|
|
|
// This technically shouldn't be possible, but issue #62 seems to have discovered a bug
|
|
// somewhere, so just add a check to make sure.
|
|
if (toAddr.amount.toqint64() < 0) {
|
|
return QString(tr("Amount for address '%1' is invalid!").arg(toAddr.addr));
|
|
ui->memoTxtChat->setEnabled(true);
|
|
}
|
|
|
|
total = total + toAddr.amount;
|
|
}
|
|
total = total + tx.fee;
|
|
|
|
auto available = rpc->getModel()->getAvailableBalance();
|
|
|
|
if (available < total) {
|
|
return tr("Not enough available funds to send this transaction\n\nHave: %1\nNeed: %2\n\nNote: Funds need 1 confirmations before they can be spent")
|
|
.arg(available.toDecimalhushString(), total.toDecimalhushString());
|
|
ui->memoTxtChat->setEnabled(true);
|
|
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
// Create a Tx from the current state of the Chat page.
|
|
Tx MainWindow::createTxFromSendRequestChatPage() {
|
|
Tx tx;
|
|
CAmount totalAmt;
|
|
// For each addr/amt in the Chat tab
|
|
{
|
|
|
|
QString amtStr = this->getAmt();
|
|
CAmount amt;
|
|
CAmount amtHm;
|
|
|
|
amt = CAmount::fromDecimalString("0");
|
|
amtHm = CAmount::fromDecimalString("0");
|
|
totalAmt = totalAmt + amt;
|
|
|
|
QModelIndex index = ui->listContactWidget->currentIndex();
|
|
QString label_contact = index.data(Qt::DisplayRole).toString();
|
|
|
|
for(auto &c : AddressBook::getInstance()->getAllAddressLabels())
|
|
|
|
if (label_contact == c.getName()) {
|
|
|
|
QString cid = c.getCid();
|
|
QString myAddr = c.getMyAddress();
|
|
QString type = "Money";
|
|
QString addr = c.getPartnerAddress();
|
|
QString moneymemo = this->getMoneyMemo();
|
|
|
|
/////////User input for chatmemos
|
|
QString memoplain = QString("Request of : ") + amtStr + QString(" HUSH ") + QString("\n") + QString("\n") + moneymemo;
|
|
|
|
/////////We convert the user input from QString to unsigned char*, so we can encrypt it later
|
|
int lengthmemo = memoplain.length();
|
|
|
|
char *memoplainchar = NULL;
|
|
memoplainchar = new char[lengthmemo+2];
|
|
strncpy(memoplainchar, memoplain.toUtf8(), lengthmemo +1);
|
|
|
|
QString pubkey = this->getPubkeyByAddress(addr);
|
|
QString passphraseHash = DataStore::getChatDataStore()->getPassword();
|
|
int length = passphraseHash.length();
|
|
|
|
////////////////Generate the secretkey for our message encryption
|
|
|
|
char *hashEncryptionKeyraw = NULL;
|
|
hashEncryptionKeyraw = new char[length+1];
|
|
strncpy(hashEncryptionKeyraw, passphraseHash.toUtf8(), length+1);
|
|
|
|
#define MESSAGEAS1 ((const unsigned char *) hashEncryptionKeyraw)
|
|
#define MESSAGEAS1_LEN length
|
|
|
|
|
|
unsigned char sk[crypto_kx_SECRETKEYBYTES];
|
|
unsigned char pk[crypto_kx_PUBLICKEYBYTES];
|
|
unsigned char server_rx[crypto_kx_SESSIONKEYBYTES], server_tx[crypto_kx_SESSIONKEYBYTES];
|
|
|
|
if (crypto_kx_seed_keypair(pk,sk, MESSAGEAS1) !=0) {
|
|
this->logger->write("Suspicious keypair, bail out ");
|
|
qDebug() << __func__<< ": Suspicious client public outgoing key from crypto_kx_seed_keypair, aborting!";
|
|
return tx;
|
|
}
|
|
////////////////Get the pubkey from Bob, so we can create the share key
|
|
|
|
const QByteArray pubkeyBobArray = QByteArray::fromHex(pubkey.toLatin1());
|
|
const unsigned char *pubkeyBob = reinterpret_cast<const unsigned char *>(pubkeyBobArray.constData());
|
|
/////Create the shared key for sending the message
|
|
|
|
if (crypto_kx_server_session_keys(server_rx, server_tx, pk, sk, pubkeyBob) != 0) {
|
|
this->logger->write("Suspicious client public send key, bail out ");
|
|
qDebug() << __func__ << ": Suspicious client public send key from crypto_kx_server_session_keys, aborting!";
|
|
return tx;
|
|
}
|
|
|
|
|
|
// Let's try to preserve Unicode characters
|
|
QByteArray ba_memo = memoplain.toUtf8();
|
|
int ba_memo_length = ba_memo.size();
|
|
|
|
#define MESSAGEMoney (const unsigned char *) ba_memo.data()
|
|
#define MESSAGE_LENMoney ba_memo_length
|
|
|
|
|
|
////////////Now lets encrypt the message Alice send to Bob//////////////////////////////
|
|
//#define MESSAGE (const unsigned char *) memoplainchar
|
|
//#define MESSAGE_LEN lengthmemo
|
|
#define CIPHERTEXT_LEN (crypto_secretstream_xchacha20poly1305_ABYTES + MESSAGE_LENMoney)
|
|
unsigned char ciphertext[CIPHERTEXT_LEN];
|
|
unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];
|
|
|
|
crypto_secretstream_xchacha20poly1305_state state;
|
|
|
|
/* Set up a new stream: initialize the state and create the header */
|
|
crypto_secretstream_xchacha20poly1305_init_push(&state, header, server_tx);
|
|
|
|
|
|
/* Now, encrypt the first chunk. `c1` will contain an encrypted,
|
|
* authenticated representation of `MESSAGE_PART1`. */
|
|
crypto_secretstream_xchacha20poly1305_push
|
|
(&state, ciphertext, NULL, MESSAGEMoney, MESSAGE_LENMoney, NULL, 0, crypto_secretstream_xchacha20poly1305_TAG_FINAL);
|
|
|
|
////Create the HM for this message
|
|
QString headerbytes = QByteArray(reinterpret_cast<const char*>(header), crypto_secretstream_xchacha20poly1305_HEADERBYTES).toHex();
|
|
QString publickeyAlice = QByteArray(reinterpret_cast<const char*>(pk), crypto_kx_PUBLICKEYBYTES).toHex();
|
|
|
|
|
|
QString hmemo= createHeaderMemo(type,cid,myAddr,headerbytes,publickeyAlice,1,0);
|
|
|
|
/////Ciphertext Memo
|
|
QString memo = QByteArray(reinterpret_cast<const char*>(ciphertext), CIPHERTEXT_LEN).toHex();
|
|
|
|
|
|
tx.toAddrs.push_back(ToFields{addr, amtHm, hmemo});
|
|
tx.toAddrs.push_back(ToFields{addr, amt, memo});
|
|
|
|
}
|
|
}
|
|
|
|
tx.fee = Settings::getMinerFee();
|
|
|
|
return tx;
|
|
|
|
}
|
|
|
|
void MainWindow::sendMoneyRequestChat() {
|
|
|
|
////////////////////////////Todo: Check if a Contact is selected//////////
|
|
|
|
// Create a Tx from the values on the send tab. Note that this Tx object
|
|
// might not be valid yet.
|
|
|
|
/* QString Name = ui->contactNameMemo->text();
|
|
|
|
if ((ui->contactNameMemo->text().isEmpty()) || (ui->memoTxtChat->toPlainText().trimmed().isEmpty())) {
|
|
|
|
QMessageBox msg(QMessageBox::Critical, tr("You have to select a contact and insert a Memo"),
|
|
tr("You have selected no Contact from Contactlist,\n") + tr("\nor your Memo is empty"),
|
|
QMessageBox::Ok, this);
|
|
|
|
msg.exec();
|
|
return;
|
|
}*/
|
|
|
|
|
|
Tx tx = createTxFromSendRequestChatPage();
|
|
|
|
QString error = doSendChatMoneyRequestTxValidations(tx);
|
|
|
|
if (!error.isEmpty()) {
|
|
// Something went wrong, so show an error and exit
|
|
QMessageBox msg(QMessageBox::Critical, tr("Message Error"), error,
|
|
QMessageBox::Ok, this);
|
|
|
|
msg.exec();
|
|
|
|
// abort the Tx
|
|
return;
|
|
}
|
|
|
|
auto movie = new QMovie(this);
|
|
auto movie1 = new QMovie(this);
|
|
movie->setFileName(":/img/res/loaderblack.gif");
|
|
movie1->setFileName(":/img/res/loaderwhite.gif");
|
|
|
|
auto theme = Settings::getInstance()->get_theme_name();
|
|
if (theme == "Dark" || theme == "Midnight") {
|
|
|
|
connect(movie, &QMovie::frameChanged, [=]{
|
|
ui->sendChatButton->setIcon(movie->currentPixmap());
|
|
});
|
|
movie->start();
|
|
ui->sendChatButton->show();
|
|
ui->sendChatButton->setEnabled(false);
|
|
|
|
} else {
|
|
|
|
connect(movie1, &QMovie::frameChanged, [=]{
|
|
ui->sendChatButton->setIcon(movie1->currentPixmap());
|
|
});
|
|
movie1->start();
|
|
ui->sendChatButton->show();
|
|
ui->sendChatButton->setEnabled(false);
|
|
}
|
|
|
|
ui->memoTxtChat->clear();
|
|
|
|
// And send the Tx
|
|
rpc->executeTransaction(tx,
|
|
[=] (QString txid) {
|
|
ui->statusBar->showMessage(Settings::txidStatusMessage + " " + txid);
|
|
|
|
|
|
QTimer::singleShot(1000, [=]() {
|
|
|
|
if (theme == "Dark" || theme == "Midnight") {
|
|
QPixmap send(":/icons/res/send-white.png");
|
|
QIcon sendIcon(send);
|
|
ui->sendChatButton->setIcon(sendIcon);
|
|
movie->stop();
|
|
ui->sendChatButton->setEnabled(true);
|
|
}else{
|
|
|
|
QPixmap send(":/icons/res/sendBlack.png");
|
|
QIcon sendIcon(send);
|
|
ui->sendChatButton->setIcon(sendIcon);
|
|
movie1->stop();
|
|
ui->sendChatButton->setEnabled(true);
|
|
}
|
|
|
|
});
|
|
|
|
// Force a UI update so we get the unconfirmed Tx
|
|
rpc->refresh(true);
|
|
ui->memoTxtChat->clear();
|
|
|
|
},
|
|
// Errored out
|
|
[=] (QString opid, QString errStr) {
|
|
ui->statusBar->showMessage(QObject::tr(" Tx ") % opid % QObject::tr(" failed"), 15 * 1000);
|
|
|
|
if (!opid.isEmpty())
|
|
errStr = QObject::tr("The transaction with id ") % opid % QObject::tr(" failed. The error was") + ":\n\n" + errStr;
|
|
|
|
QMessageBox::critical(this, QObject::tr("Transaction Error"), errStr, QMessageBox::Ok);
|
|
movie->stop();
|
|
|
|
|
|
if (theme == "Dark" || theme == "Midnight") {
|
|
QPixmap send(":/icons/res/send-white.png");
|
|
QIcon sendIcon(send);
|
|
ui->sendChatButton->setIcon(sendIcon);
|
|
movie->stop();
|
|
ui->sendChatButton->setEnabled(true);
|
|
}else{
|
|
|
|
QPixmap send(":/icons/res/sendBlack.png");
|
|
QIcon sendIcon(send);
|
|
ui->sendChatButton->setIcon(sendIcon);
|
|
movie1->stop();
|
|
ui->sendChatButton->setEnabled(true);
|
|
}
|
|
|
|
|
|
|
|
}
|
|
);
|
|
|
|
}
|
|
|
|
QString MainWindow::doSendChatMoneyRequestTxValidations(Tx tx) {
|
|
// Check to see if we have enough verified funds to send the Tx.
|
|
|
|
CAmount total;
|
|
for (auto toAddr : tx.toAddrs) {
|
|
if (!Settings::isValidAddress(toAddr.addr)) {
|
|
QString addr = (toAddr.addr.length() > 100 ? toAddr.addr.left(100) + "..." : toAddr.addr);
|
|
return QString(tr("Recipient Address ")) % addr % tr(" is Invalid");
|
|
}
|
|
|
|
// This technically shouldn't be possible, but issue #62 seems to have discovered a bug
|
|
// somewhere, so just add a check to make sure.
|
|
if (toAddr.amount.toqint64() < 0) {
|
|
return QString(tr("Amount for address '%1' is invalid!").arg(toAddr.addr));
|
|
}
|
|
|
|
total = total + toAddr.amount;
|
|
}
|
|
total = total + tx.fee;
|
|
|
|
auto available = rpc->getModel()->getAvailableBalance();
|
|
|
|
if (available < total) {
|
|
return tr("Not enough available funds to send this transaction\n\nHave: %1\nNeed: %2\n\nNote: Funds need 1 confirmations before they can be spent")
|
|
.arg(available.toDecimalhushString(), total.toDecimalhushString());
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
|
|
void MainWindow::updateChat()
|
|
{
|
|
rpc->refreshChat(ui->listChat,ui->memoSizeChat);
|
|
rpc->refresh(true);
|
|
}
|
|
|
|
void MainWindow::updateContacts()
|
|
{
|
|
|
|
}
|
|
|
|
void MainWindow::addNewZaddr(bool sapling) {
|
|
rpc->createNewZaddr(sapling, [=] (json reply) {
|
|
QString addr = QString::fromStdString(reply.get<json::array_t>()[0]);
|
|
// Make sure the RPC class reloads the z-addrs for future use
|
|
rpc->refreshAddresses();
|
|
|
|
// Just double make sure the z-address is still checked
|
|
if ( sapling && ui->rdioZSAddr->isChecked() ) {
|
|
ui->listReceiveAddresses->insertItem(0, addr);
|
|
ui->listReceiveAddresses->setCurrentIndex(0);
|
|
ui->statusBar->showMessage(QString::fromStdString("Created new zaddr"), 10 * 1000);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// Adds sapling z-addresses to the combo box. Technically, returns a
|
|
// lambda, which can be connected to the appropriate signal
|
|
std::function<void(bool)> MainWindow::addZAddrsToComboList(bool sapling) {
|
|
return [=] (bool checked) {
|
|
if (checked) {
|
|
auto addrs = this->rpc->getModel()->getAllZAddresses();
|
|
|
|
// Save the current address, so we can update it later
|
|
auto zaddr = ui->listReceiveAddresses->currentText();
|
|
ui->listReceiveAddresses->clear();
|
|
|
|
std::for_each(addrs.begin(), addrs.end(), [=] (auto addr) {
|
|
if ( (sapling && Settings::getInstance()->isSaplingAddress(addr)) ||
|
|
(!sapling && !Settings::getInstance()->isSaplingAddress(addr))) {
|
|
auto bal = rpc->getModel()->getAllBalances().value(addr);
|
|
ui->listReceiveAddresses->addItem(addr, bal);
|
|
}
|
|
});
|
|
|
|
if (!zaddr.isEmpty() && Settings::isZAddress(zaddr)) {
|
|
ui->listReceiveAddresses->setCurrentText(zaddr);
|
|
}
|
|
|
|
// If z-addrs are empty, then create a new one.
|
|
if (addrs.isEmpty()) {
|
|
addNewZaddr(sapling);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
void MainWindow::setupReceiveTab() {
|
|
// View all addresses goes to "View all private keys"
|
|
QObject::connect(ui->btnViewAllAddresses, &QPushButton::clicked, [=] () {
|
|
// If there's no RPC, return
|
|
if (!getRPC())
|
|
return;
|
|
|
|
QDialog d(this);
|
|
Ui_ViewAddressesDialog viewaddrs;
|
|
viewaddrs.setupUi(&d);
|
|
Settings::saveRestore(&d);
|
|
Settings::saveRestoreTableHeader(viewaddrs.tblAddresses, &d, "viewalladdressestable");
|
|
viewaddrs.tblAddresses->horizontalHeader()->setStretchLastSection(true);
|
|
|
|
QList<QString> allAddresses;
|
|
allAddresses = getRPC()->getModel()->getAllZAddresses();
|
|
|
|
ViewAllAddressesModel model(viewaddrs.tblAddresses, allAddresses, getRPC());
|
|
viewaddrs.tblAddresses->setModel(&model);
|
|
|
|
QObject::connect(viewaddrs.btnExportAll, &QPushButton::clicked, this, &MainWindow::exportAllKeys);
|
|
|
|
viewaddrs.tblAddresses->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
QObject::connect(viewaddrs.tblAddresses, &QTableView::customContextMenuRequested, [=] (QPoint pos) {
|
|
QModelIndex index = viewaddrs.tblAddresses->indexAt(pos);
|
|
if (index.row() < 0) return;
|
|
|
|
index = index.sibling(index.row(), 0);
|
|
QString addr = viewaddrs.tblAddresses->model()->data(index).toString();
|
|
|
|
QMenu menu(this);
|
|
menu.addAction(tr("Export Private Key"), [=] () {
|
|
if (addr.isEmpty())
|
|
return;
|
|
|
|
this->exportKeys(addr);
|
|
});
|
|
menu.addAction(tr("Copy Address"), [=]() {
|
|
QGuiApplication::clipboard()->setText(addr);
|
|
});
|
|
menu.exec(viewaddrs.tblAddresses->viewport()->mapToGlobal(pos));
|
|
});
|
|
|
|
d.exec();
|
|
});
|
|
|
|
QObject::connect(ui->rdioZSAddr, &QRadioButton::toggled, addZAddrsToComboList(true));
|
|
|
|
// Explicitly get new address button.
|
|
QObject::connect(ui->btnReceiveNewAddr, &QPushButton::clicked, [=] () {
|
|
if (!rpc->getConnection())
|
|
return;
|
|
|
|
// Go over the dropdown and just select the next address that has:
|
|
// 0 balance and has no labels
|
|
for (int i=ui->listReceiveAddresses->currentIndex()+1; i < ui->listReceiveAddresses->count(); i++) {
|
|
QString item = ui->listReceiveAddresses->itemText(i);
|
|
CAmount bal = getRPC()->getModel()->getAllBalances().value(item, CAmount());
|
|
if (bal == 0 && AddressBook::getInstance()->getLabelForAddress(item).isEmpty()) {
|
|
// Pick this one, since it has no balance and no label
|
|
ui->listReceiveAddresses->setCurrentIndex(i);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If none of the existing items were eligible, create a new one.
|
|
|
|
if (ui->rdioZSAddr->isChecked()) {
|
|
addNewZaddr(true);
|
|
}
|
|
});
|
|
|
|
// Focus enter for the Receive Tab
|
|
QObject::connect(ui->tabWidget, &QTabWidget::currentChanged, [=] (int tab) {
|
|
if (tab == 3) {
|
|
// Switched to receive tab, select the z-addr radio button
|
|
ui->rdioZSAddr->setChecked(true);
|
|
|
|
|
|
// And then select the first one
|
|
ui->listReceiveAddresses->setCurrentIndex(0);
|
|
}
|
|
});
|
|
|
|
// Validator for label
|
|
QRegExpValidator* v = new QRegExpValidator(QRegExp(Settings::labelRegExp), ui->rcvLabel);
|
|
ui->rcvLabel->setValidator(v);
|
|
|
|
// Select item in address list
|
|
QObject::connect(ui->listReceiveAddresses,
|
|
QOverload<int>::of(&QComboBox::currentIndexChanged), [=] (int index) {
|
|
QString addr = ui->listReceiveAddresses->itemText(index);
|
|
if (addr.isEmpty()) {
|
|
// Draw empty stuff
|
|
|
|
ui->rcvLabel->clear();
|
|
ui->rcvBal->clear();
|
|
ui->txtReceive->clear();
|
|
ui->qrcodeDisplay->clear();
|
|
return;
|
|
}
|
|
|
|
auto label = AddressBook::getInstance()->getLabelForAddress(addr);
|
|
if (label.isEmpty()) {
|
|
ui->rcvUpdateLabel->setText("Add Label");
|
|
}
|
|
else {
|
|
ui->rcvUpdateLabel->setText("Update Label");
|
|
}
|
|
|
|
ui->rcvLabel->setText(label);
|
|
if (Settings::getInstance()->get_currency_name() == "USD") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushUSDString());
|
|
} else if (Settings::getInstance()->get_currency_name() == "EUR") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushEURString());
|
|
} else if (Settings::getInstance()->get_currency_name() == "BTC") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushBTCString());
|
|
} else if (Settings::getInstance()->get_currency_name() == "CNY") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushCNYString());
|
|
} else if (Settings::getInstance()->get_currency_name() == "RUB") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushRUBString());
|
|
} else if (Settings::getInstance()->get_currency_name() == "CAD") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushCADString());
|
|
} else if (Settings::getInstance()->get_currency_name() == "SGD") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushSGDString());
|
|
} else if (Settings::getInstance()->get_currency_name() == "CHF") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushCHFString());
|
|
} else if (Settings::getInstance()->get_currency_name() == "INR") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushINRString());
|
|
} else if (Settings::getInstance()->get_currency_name() == "GBP") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushGBPString());
|
|
} else if (Settings::getInstance()->get_currency_name() == "AUD") {
|
|
ui->rcvBal->setText(rpc->getModel()->getAllBalances().value(addr).toDecimalhushAUDString());
|
|
}
|
|
|
|
ui->txtReceive->setPlainText(addr);
|
|
ui->qrcodeDisplay->setQrcodeString(addr);
|
|
if (rpc->getModel()->getUsedAddresses().value(addr, false)) {
|
|
ui->rcvBal->setToolTip(tr("Address has been previously used"));
|
|
} else {
|
|
ui->rcvBal->setToolTip(tr("Address is unused"));
|
|
}
|
|
|
|
});
|
|
|
|
// Receive tab add/update label
|
|
QObject::connect(ui->rcvUpdateLabel, &QPushButton::clicked, [=]() {
|
|
QString addr = ui->listReceiveAddresses->currentText();
|
|
|
|
if (addr.isEmpty())
|
|
return;
|
|
|
|
auto curLabel = AddressBook::getInstance()->getLabelForAddress(addr);
|
|
auto label = ui->rcvLabel->text().trimmed();
|
|
|
|
if (curLabel == label) // Nothing to update
|
|
return;
|
|
|
|
QString info;
|
|
|
|
if (!curLabel.isEmpty() && label.isEmpty()) {
|
|
info = "Removed Label '" % curLabel % "'";
|
|
AddressBook::getInstance()->removeAddressLabel(curLabel, addr, "", "","" );
|
|
}
|
|
else if (!curLabel.isEmpty() && !label.isEmpty()) {
|
|
info = "Updated Label '" % curLabel % "' to '" % label % "'";
|
|
AddressBook::getInstance()->updateLabel(curLabel, addr, label);
|
|
}
|
|
else if (curLabel.isEmpty() && !label.isEmpty()) {
|
|
info = "Added Label '" % label % "'";
|
|
AddressBook::getInstance()->addAddressLabel(label, addr, "", "", "");
|
|
}
|
|
|
|
// Update labels everywhere on the UI
|
|
updateLabels();
|
|
|
|
// Show the user feedback
|
|
if (!info.isEmpty()) {
|
|
QMessageBox::information(this, "Label", info, QMessageBox::Ok);
|
|
}
|
|
});
|
|
|
|
// Receive Export Key
|
|
QObject::connect(ui->exportKey, &QPushButton::clicked, [=]() {
|
|
QString addr = ui->listReceiveAddresses->currentText();
|
|
if (addr.isEmpty())
|
|
return;
|
|
|
|
this->exportKeys(addr);
|
|
});
|
|
}
|
|
|
|
// Updates the labels everywhere on the UI. Call this after the labels have been updated
|
|
void MainWindow::updateLabels() {
|
|
// Update the Receive tab
|
|
addZAddrsToComboList(ui->rdioZSAddr->isChecked())(true);
|
|
|
|
// Update the autocomplete
|
|
updateLabelsAutoComplete();
|
|
}
|
|
|
|
void MainWindow::slot_change_currency(const QString& currency_name) {
|
|
|
|
Settings::getInstance()->set_currency_name(currency_name);
|
|
|
|
// Include currency
|
|
|
|
QString saved_currency_name;
|
|
try
|
|
{
|
|
saved_currency_name = Settings::getInstance()->get_currency_name();
|
|
|
|
} catch (...) {
|
|
saved_currency_name = "BTC";
|
|
}
|
|
}
|
|
|
|
void MainWindow::slot_change_theme(const QString& theme_name) {
|
|
Settings::getInstance()->set_theme_name(theme_name);
|
|
|
|
|
|
// Include css
|
|
QString saved_theme_name;
|
|
try
|
|
{
|
|
saved_theme_name = Settings::getInstance()->get_theme_name();
|
|
}
|
|
catch (...)
|
|
{
|
|
saved_theme_name = "Dark";
|
|
}
|
|
|
|
QFile qFile(":/css/res/css/" + saved_theme_name +".css");
|
|
if (qFile.open(QFile::ReadOnly))
|
|
{
|
|
QString styleSheet = QLatin1String(qFile.readAll());
|
|
this->setStyleSheet(""); // resets styles, makes app restart unnecessary
|
|
this->setStyleSheet(styleSheet);
|
|
}
|
|
|
|
}
|
|
|
|
MainWindow::~MainWindow()
|
|
{
|
|
delete ui;
|
|
delete rpc;
|
|
delete labelCompleter;
|
|
|
|
delete sendTxRecurringInfo;
|
|
delete amtValidator;
|
|
delete feesValidator;
|
|
|
|
delete loadingMovie;
|
|
delete logger;
|
|
|
|
delete wsserver;
|
|
delete wormhole;
|
|
}
|
|
void MainWindow::on_givemeZaddr_clicked()
|
|
{
|
|
|
|
bool sapling = true;
|
|
rpc->createNewZaddr(sapling, [=] (json reply) {
|
|
QString hushchataddr = QString::fromStdString(reply.get<json::array_t>()[0]);
|
|
QClipboard *zaddr_Clipboard = QApplication::clipboard();
|
|
zaddr_Clipboard ->setText(hushchataddr);
|
|
QMessageBox::information(this, "Your new HushChat address was copied to your clipboard!",hushchataddr);
|
|
ui->listReceiveAddresses->insertItem(0, hushchataddr);
|
|
ui->listReceiveAddresses->setCurrentIndex(0);
|
|
qDebug() << __func__ << ": hushchat zaddr=" << hushchataddr << " created";
|
|
|
|
});
|
|
}
|
|
|
|
|
|
// TODO: The way emoji work really need to change, this is madness
|
|
void MainWindow::on_emojiButton_clicked()
|
|
{
|
|
|
|
QDialog emojiDialog(this);
|
|
Ui_emojiDialog emoji;
|
|
emoji.setupUi(&emojiDialog);
|
|
Settings::saveRestore(&emojiDialog);
|
|
|
|
QObject::connect(emoji.smiley, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":a1");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.money, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":a2");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.laughing, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":a3");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.sweet_smile, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":a4");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.joy, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":a5");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.innocent, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":a6");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.partying_face, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":a7");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.rolling_eyes, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":a8");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.tongue, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":a9");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.hearts3, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":b1");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.heart_eyes, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":b2");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.nauseated, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":b3");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.poop, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":b4");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.symbols_mouth, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":b5");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.sunglass, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":b6");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.stuck_out, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":b7");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.hush_white, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":b8");
|
|
|
|
|
|
});
|
|
|
|
QObject::connect(emoji.sd, &QPushButton::clicked, [&] () {
|
|
ui->memoTxtChat->insertHtml(":b9");
|
|
|
|
|
|
});
|
|
|
|
emojiDialog.exec();
|
|
}
|
|
|
|
|