434 lines
26 KiB
C++
434 lines
26 KiB
C++
#include "wallet_store.h"
|
|
#include <QtWidgets>
|
|
#include <QtNetwork>
|
|
#include <sodium.h>
|
|
#include <functional>
|
|
|
|
namespace {
|
|
constexpr qint64 Coin = 100000000;
|
|
constexpr qint64 MaxMoney = 209000000LL * Coin;
|
|
QString money(qint64 value) {
|
|
return QString::number(value / Coin) + "." + QString::number(qAbs(value % Coin)).rightJustified(8, '0');
|
|
}
|
|
qint64 parseMoney(const QString &text) {
|
|
static const QRegularExpression pattern("^(0|[1-9][0-9]{0,8})(\\.[0-9]{1,8})?$");
|
|
if (!pattern.match(text).hasMatch()) walletError("Enter an amount with up to eight decimal places.");
|
|
const auto parts = text.split('.');
|
|
qint64 value = parts[0].toLongLong() * Coin;
|
|
if (parts.size() == 2) value += parts[1].leftJustified(8, '0').toLongLong();
|
|
if (value > MaxMoney) walletError("Amount is outside the NRB money range.");
|
|
return value;
|
|
}
|
|
qint64 atomicAmount(const QJsonValue &v) {
|
|
bool ok = false;
|
|
qint64 value = v.toString().toLongLong(&ok);
|
|
if (!ok || value < 0 || value > MaxMoney) walletError("The network returned an invalid amount.");
|
|
return value;
|
|
}
|
|
QByteArray askPassword(QWidget *parent, const QString &title, const QString &text) {
|
|
QInputDialog dialog(parent);
|
|
dialog.setWindowTitle(title); dialog.setLabelText(text);
|
|
dialog.setTextEchoMode(QLineEdit::Password); dialog.resize(460, 160);
|
|
if (dialog.exec() != QDialog::Accepted) return {};
|
|
QString password = dialog.textValue();
|
|
QByteArray bytes = password.toUtf8();
|
|
password.fill(QChar(0)); dialog.setTextValue({});
|
|
return bytes;
|
|
}
|
|
QLabel *textLabel(const QString &text) {
|
|
auto *label = new QLabel(text); label->setWordWrap(true); return label;
|
|
}
|
|
|
|
class WalletWindow : public QMainWindow {
|
|
WalletStore store;
|
|
QNetworkAccessManager manager;
|
|
QTimer refreshTimer, idleTimer;
|
|
QLabel *connection, *balance, *pending, *receiveStatus, *walletStatus, *sendStatus;
|
|
QLabel *receiveAddress;
|
|
QLineEdit *labelEdit, *recipient, *amountEdit, *feeEdit;
|
|
QListWidget *addresses, *transactions;
|
|
QPushButton *newAddress, *unlockButton, *sendButton;
|
|
QTabWidget *tabs;
|
|
bool busy = false;
|
|
bool refreshing = false;
|
|
bool online = false;
|
|
int sessionGeneration = 0;
|
|
QString lastTxid;
|
|
|
|
void report(const std::exception &e) {
|
|
QMessageBox::warning(this, "Nerbus Wallet", QString::fromUtf8(e.what()));
|
|
}
|
|
void guarded(const std::function<void()> &fn) {
|
|
try { fn(); } catch (const std::exception &e) { report(e); }
|
|
}
|
|
QJsonArray publicAddresses() const {
|
|
QJsonArray result;
|
|
for (auto item : store.entries()) result.append(item.toObject()["address"]);
|
|
return result;
|
|
}
|
|
void api(QJsonObject request, const std::function<void(QJsonObject, QString)> &done) {
|
|
QNetworkRequest req(QUrl("https://explorer.nerbus.com/wallet/v1"));
|
|
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
|
req.setRawHeader("User-Agent", "NerbusWallet/0.2.1");
|
|
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
|
|
req.setTransferTimeout(30000);
|
|
auto *reply = manager.post(req, QJsonDocument(request).toJson(QJsonDocument::Compact));
|
|
connect(reply, &QNetworkReply::readyRead, this, [reply] {
|
|
if (reply->bytesAvailable() > 16 * 1024 * 1024) reply->abort();
|
|
});
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply, done] {
|
|
QByteArray bytes = reply->readAll();
|
|
auto doc = QJsonDocument::fromJson(bytes);
|
|
auto result = doc.object();
|
|
QString error;
|
|
if (bytes.size() > 16 * 1024 * 1024 || !doc.isObject())
|
|
error = "Network service is unavailable.";
|
|
else if (result.contains("error")) error = result["error"].toString("Network request failed.").left(300);
|
|
else if (reply->error() != QNetworkReply::NoError) error = "Cannot connect to the NRB network service.";
|
|
else if (result["genesis"].toString() != NrbGenesis) error = "Network identity does not match NRB.";
|
|
reply->deleteLater();
|
|
try { done(result, error); } catch (const std::exception &e) { busy = false; updateButtons(); report(e); }
|
|
});
|
|
}
|
|
void updateButtons() {
|
|
newAddress->setEnabled(!busy);
|
|
sendButton->setEnabled(!busy && online && store.unlocked());
|
|
unlockButton->setText(store.unlocked() ? (store.passwordProtected() ? "Lock wallet" : "Wallet open")
|
|
: store.exists() ? "Open wallet" : "Create wallet");
|
|
unlockButton->setEnabled(!busy && (!store.unlocked() || store.passwordProtected()));
|
|
walletStatus->setText(store.unlocked() ? (store.passwordProtected() ? "Wallet unlocked. Password protection is on."
|
|
: "Wallet open. Password protection is off.")
|
|
: store.exists() ? "Open your wallet to view your addresses." : "Create a wallet or restore a backup.");
|
|
}
|
|
void walletOpened() {
|
|
++sessionGeneration;
|
|
if (store.passwordProtected()) idleTimer.start(); else idleTimer.stop();
|
|
populateAddresses(); updateButtons(); refresh();
|
|
}
|
|
bool ensureUnlocked() {
|
|
if (busy) return false;
|
|
if (store.unlocked()) { if (store.passwordProtected()) idleTimer.start(); return true; }
|
|
if (!store.exists()) {
|
|
store.create({});
|
|
} else if (!WalletStore::fileRequiresPassword(store.path())) {
|
|
store.unlock({});
|
|
} else {
|
|
QByteArray password = askPassword(this, "Unlock wallet", "Wallet password:");
|
|
if (password.isEmpty()) return false;
|
|
store.unlock(std::move(password));
|
|
}
|
|
walletOpened();
|
|
return true;
|
|
}
|
|
void setPassword() {
|
|
if (!ensureUnlocked()) return;
|
|
QByteArray password = askPassword(this, "Set wallet password", "New wallet password (optional protection):");
|
|
if (password.isEmpty()) return;
|
|
QByteArray confirmation = askPassword(this, "Confirm password", "Enter the same password again:");
|
|
const bool same = password == confirmation;
|
|
wipe(confirmation);
|
|
if (!same) { wipe(password); walletError("Passwords do not match."); }
|
|
store.changePassword(std::move(password));
|
|
idleTimer.start(); updateButtons();
|
|
walletStatus->setText("Password protection is on. Existing backup files retain their previous protection.");
|
|
}
|
|
void removePassword() {
|
|
if (!ensureUnlocked()) return;
|
|
if (store.passwordProtected()) store.changePassword({});
|
|
idleTimer.stop(); updateButtons();
|
|
}
|
|
void lockWallet() {
|
|
if (busy || !store.passwordProtected()) return;
|
|
++sessionGeneration; idleTimer.stop(); store.lock();
|
|
addresses->clear(); transactions->clear(); receiveAddress->setText("Unlock your wallet to view your addresses.");
|
|
receiveStatus->clear(); balance->setText("— NRB"); pending->clear();
|
|
updateButtons();
|
|
}
|
|
void populateAddresses(const QString &selected = {}) {
|
|
addresses->clear();
|
|
if (!store.unlocked()) return;
|
|
for (auto item : store.entries()) {
|
|
const auto obj = item.toObject();
|
|
QString address = obj["address"].toString();
|
|
QString label = obj["label"].toString();
|
|
auto *row = new QListWidgetItem(label.isEmpty() ? address : label + "\n" + address, addresses);
|
|
row->setData(Qt::UserRole, address);
|
|
if (address == selected) addresses->setCurrentItem(row);
|
|
}
|
|
if (!addresses->currentItem() && addresses->count()) addresses->setCurrentRow(addresses->count() - 1);
|
|
}
|
|
void makeAddress() {
|
|
try {
|
|
if (!ensureUnlocked()) return;
|
|
busy = true; updateButtons();
|
|
const auto pair = offlineCore({{"action", "newKey"}});
|
|
QString address = store.addKey(pair, labelEdit->text().trimmed());
|
|
populateAddresses(address); labelEdit->clear(); receiveStatus->clear();
|
|
busy = false; updateButtons();
|
|
} catch (const std::exception &e) {
|
|
busy = false; updateButtons(); receiveStatus->setText(QString::fromUtf8(e.what()));
|
|
}
|
|
}
|
|
void refresh() {
|
|
if (refreshing || busy) return;
|
|
refreshing = true;
|
|
const int generation = sessionGeneration;
|
|
QJsonObject request{{"action", "status"}};
|
|
if (store.unlocked() && !store.entries().empty()) {
|
|
request["action"] = "account"; request["addresses"] = publicAddresses();
|
|
}
|
|
api(request, [this, generation](QJsonObject result, QString error) {
|
|
refreshing = false;
|
|
if (generation != sessionGeneration) return;
|
|
online = error.isEmpty() && result["ready"].toBool();
|
|
connection->setText(error.isEmpty() ? (online ? "Connected · Block " + QString::number(result["height"].toInt())
|
|
: "Waiting for the NRB network")
|
|
: "Network unavailable · Addresses remain available");
|
|
if (!error.isEmpty()) connection->setToolTip(error);
|
|
else connection->setToolTip("Network information is provided by explorer.nerbus.com.");
|
|
if (store.unlocked() && result.contains("balance") && error.isEmpty()) {
|
|
balance->setText(money(atomicAmount(result["balance"])) + " NRB");
|
|
pending->setText("Confirmed balance · some outputs may still be immature");
|
|
transactions->clear();
|
|
for (const auto &entry : result["transactions"].toArray()) {
|
|
QString txid = entry.toString();
|
|
if (QRegularExpression("^[a-f0-9]{64}$").match(txid).hasMatch())
|
|
transactions->addItem(txid);
|
|
}
|
|
} else if (!error.isEmpty()) {
|
|
balance->setText("— NRB"); pending->setText("Balance will update when the network is available.");
|
|
} else if (store.unlocked() && store.entries().empty()) {
|
|
balance->setText("0.00000000 NRB"); pending->clear();
|
|
}
|
|
updateButtons();
|
|
});
|
|
}
|
|
void backup() {
|
|
if (!store.exists()) { if (!ensureUnlocked()) return; }
|
|
const QString path = QFileDialog::getSaveFileName(this, "Back up wallet",
|
|
QDir::homePath() + "/Nerbus-backup-" + QDate::currentDate().toString("yyyy-MM-dd") + ".nrb", "Nerbus wallet (*.nrb)");
|
|
if (path.isEmpty()) return;
|
|
store.backup(path);
|
|
walletStatus->setText("Backup saved. Back up again after creating or importing new addresses.");
|
|
}
|
|
void restore() {
|
|
if (store.exists()) walletError("An existing wallet will not be replaced. Start with --wallet-dir=<new folder> to restore a separate wallet.");
|
|
const QString source = QFileDialog::getOpenFileName(this, "Restore wallet", {}, "Nerbus wallet (*.nrb)");
|
|
if (source.isEmpty()) return;
|
|
QByteArray password;
|
|
if (WalletStore::fileRequiresPassword(source)) {
|
|
password = askPassword(this, "Restore wallet", "Password for this encrypted backup:");
|
|
if (password.isEmpty()) return;
|
|
}
|
|
store.restore(source, std::move(password)); walletOpened();
|
|
}
|
|
void importKey() {
|
|
if (!ensureUnlocked()) return;
|
|
QByteArray value = askPassword(this, "Import private key", "NRB private key (WIF):");
|
|
if (value.isEmpty()) return;
|
|
QJsonObject request{{"action", "importKey"}, {"privateKey", QString::fromUtf8(value)}};
|
|
wipe(value);
|
|
auto pair = offlineCore(request); request = {};
|
|
QString address = store.addKey(pair, "Imported");
|
|
populateAddresses(address); tabs->setCurrentIndex(1); refresh();
|
|
}
|
|
void exportKey() {
|
|
if (!ensureUnlocked()) return;
|
|
auto *current = addresses->currentItem();
|
|
if (!current) walletError("Select a receiving address first.");
|
|
const QString address = current->data(Qt::UserRole).toString();
|
|
if (QMessageBox::warning(this, "Export private key",
|
|
"Anyone with this private key can spend the coins at this address. Show it?",
|
|
QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel) != QMessageBox::Yes) return;
|
|
for (const auto &entry : store.entries()) if (entry.toObject()["address"] == address) {
|
|
QDialog dialog(this); dialog.setWindowTitle("NRB private key");
|
|
QVBoxLayout layout(&dialog); QLabel info("Private key for " + address);
|
|
QLineEdit edit(entry.toObject()["privateKey"].toString()); edit.setReadOnly(true);
|
|
QPushButton close("Close"); layout.addWidget(&info); layout.addWidget(&edit); layout.addWidget(&close);
|
|
connect(&close, &QPushButton::clicked, &dialog, &QDialog::accept);
|
|
dialog.resize(620, 150); dialog.exec(); edit.clear(); return;
|
|
}
|
|
}
|
|
void sendPayment() {
|
|
if (!ensureUnlocked()) return;
|
|
const QString to = recipient->text().trimmed();
|
|
if (!QRegularExpression("^[1-9A-HJ-NP-Za-km-z]{26,36}$").match(to).hasMatch())
|
|
walletError("Enter a transparent NRB receiving address.");
|
|
const qint64 amount = parseMoney(amountEdit->text().trimmed());
|
|
const qint64 fee = parseMoney(feeEdit->text().trimmed());
|
|
if (amount <= 0 || fee < 10000 || fee > Coin) walletError("Amount must be positive. The fee must be between 0.00010000 and 1 NRB.");
|
|
const QJsonArray owned = publicAddresses();
|
|
if (owned.empty()) walletError("Create a receiving address first.");
|
|
busy = true; updateButtons(); sendStatus->setText("Preparing payment…");
|
|
api({{"action", "inputs"}, {"addresses", owned}, {"target", QString::number(amount + fee)}},
|
|
[this, to, amount, fee](QJsonObject result, QString error) {
|
|
if (!error.isEmpty()) { busy = false; updateButtons(); sendStatus->setText(error); return; }
|
|
if (!result["ready"].toBool()) walletError("The network is not ready for payments.");
|
|
const auto inputs = result["inputs"].toArray();
|
|
qint64 total = 0;
|
|
QJsonArray signingInputs;
|
|
for (const auto &entry : inputs) {
|
|
auto in = entry.toObject();
|
|
qint64 value = atomicAmount(in["satoshis"]);
|
|
if (total > MaxMoney - value) walletError("Invalid total input value.");
|
|
total += value;
|
|
in["amount"] = money(value); signingInputs.append(in);
|
|
}
|
|
qint64 effectiveFee = fee;
|
|
if (total < amount + fee) walletError("Insufficient spendable funds.");
|
|
if (total - amount - fee > 0 && total - amount - fee < 1000) effectiveFee = total - amount;
|
|
const QString summary = "Send " + money(amount) + " NRB\nto " + to +
|
|
"\n\nNetwork fee: " + money(effectiveFee) + " NRB\nTotal: " + money(amount + effectiveFee) + " NRB";
|
|
if (QMessageBox::question(this, "Confirm payment", summary, QMessageBox::Yes | QMessageBox::Cancel,
|
|
QMessageBox::Cancel) != QMessageBox::Yes) { busy = false; updateButtons(); sendStatus->clear(); return; }
|
|
QJsonArray keys;
|
|
const auto entries = store.entries();
|
|
for (const auto &entry : entries) keys.append(entry.toObject()["privateKey"]);
|
|
QJsonObject signedTx = offlineCore({{"action", "payment"}, {"inputs", signingInputs}, {"privateKeys", keys},
|
|
{"height", result["height"].toInt() + 1}, {"to", to}, {"amount", money(amount)}, {"fee", money(effectiveFee)},
|
|
{"change", entries.first().toObject()["address"]}});
|
|
keys = {};
|
|
lastTxid = signedTx["txid"].toString();
|
|
// Retain a public signed transaction so a timeout cannot hide what was submitted.
|
|
const QString receiptDir = QFileInfo(store.path()).absolutePath() + "/payments";
|
|
if (!QDir().mkpath(receiptDir)) walletError("Cannot save the payment receipt.");
|
|
QSaveFile receipt(receiptDir + "/" + lastTxid + ".json");
|
|
QByteArray receiptData = QJsonDocument(signedTx).toJson();
|
|
if (!receipt.open(QIODevice::WriteOnly) || receipt.write(receiptData) != receiptData.size() || !receipt.commit())
|
|
walletError("Cannot save the payment receipt.");
|
|
sendStatus->setText("Submitting transaction " + lastTxid + "…");
|
|
api({{"action", "broadcast"}, {"hex", signedTx["hex"]}}, [this](QJsonObject reply, QString error) {
|
|
busy = false; updateButtons();
|
|
if (!error.isEmpty()) {
|
|
sendStatus->setText("Submission was not confirmed. Check transaction " + lastTxid +
|
|
" in the explorer before sending again. " + error);
|
|
} else if (reply["txid"].toString() != lastTxid) {
|
|
sendStatus->setText("Unexpected transaction response. Check " + lastTxid + " in the explorer before sending again.");
|
|
} else {
|
|
sendStatus->setText("Transaction submitted: " + lastTxid); amountEdit->clear(); refresh();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
protected:
|
|
bool eventFilter(QObject *obj, QEvent *event) override {
|
|
if (store.passwordProtected() && (event->type() == QEvent::KeyPress || event->type() == QEvent::MouseButtonPress)) idleTimer.start();
|
|
return QMainWindow::eventFilter(obj, event);
|
|
}
|
|
public:
|
|
explicit WalletWindow(QString path) : store(std::move(path)) {
|
|
setWindowTitle("Nerbus Wallet"); resize(1000, 720);
|
|
setWindowIcon(QIcon(QCoreApplication::applicationDirPath() + "/nerbus.svg"));
|
|
auto *root = new QWidget; auto *layout = new QVBoxLayout(root); layout->setContentsMargins(28, 20, 28, 20);
|
|
auto *masthead = new QHBoxLayout;
|
|
auto *logo = new QLabel; logo->setPixmap(QIcon(QCoreApplication::applicationDirPath() + "/nerbus.svg").pixmap(64, 64));
|
|
auto *name = new QLabel("NERBUS"); name->setObjectName("masthead");
|
|
masthead->addWidget(logo); masthead->addWidget(name); masthead->addStretch();
|
|
auto *unit = new QLabel("THE NRB WALLET\nYour keys. Your coins."); unit->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
|
masthead->addWidget(unit); layout->addLayout(masthead);
|
|
connection = textLabel("Connecting to the NRB network…"); layout->addWidget(connection);
|
|
tabs = new QTabWidget; layout->addWidget(tabs, 1);
|
|
|
|
auto *overview = new QWidget; auto *ov = new QVBoxLayout(overview);
|
|
ov->addWidget(textLabel("BALANCE")); balance = new QLabel("— NRB"); balance->setObjectName("balance"); ov->addWidget(balance);
|
|
pending = textLabel(""); ov->addWidget(pending); ov->addSpacing(20);
|
|
ov->addWidget(textLabel("Receive NRB using an address from your wallet. Your private keys stay on this computer."));
|
|
auto *receiveShortcut = new QPushButton("Receive NRB"); ov->addWidget(receiveShortcut, 0, Qt::AlignLeft); ov->addStretch();
|
|
tabs->addTab(overview, "Overview");
|
|
|
|
auto *receive = new QWidget; auto *rv = new QVBoxLayout(receive);
|
|
labelEdit = new QLineEdit; labelEdit->setPlaceholderText("Address label (optional)");
|
|
newAddress = new QPushButton("New receiving address"); auto *newRow = new QHBoxLayout;
|
|
newRow->addWidget(labelEdit, 1); newRow->addWidget(newAddress); rv->addLayout(newRow);
|
|
receiveAddress = textLabel("Click New receiving address to receive NRB.");
|
|
receiveAddress->setTextInteractionFlags(Qt::TextSelectableByMouse); receiveAddress->setObjectName("address"); rv->addWidget(receiveAddress);
|
|
auto *copy = new QPushButton("Copy address"); rv->addWidget(copy, 0, Qt::AlignLeft);
|
|
receiveStatus = textLabel(""); receiveStatus->setStyleSheet("color:#8b241d"); rv->addWidget(receiveStatus);
|
|
addresses = new QListWidget; rv->addWidget(addresses, 1); tabs->addTab(receive, "Receive");
|
|
connect(addresses, &QListWidget::currentItemChanged, this, [this](QListWidgetItem *current) {
|
|
if (current) receiveAddress->setText(current->data(Qt::UserRole).toString());
|
|
});
|
|
connect(newAddress, &QPushButton::clicked, this, [this] { makeAddress(); });
|
|
connect(receiveShortcut, &QPushButton::clicked, this, [this] { tabs->setCurrentIndex(1); });
|
|
connect(copy, &QPushButton::clicked, this, [this] {
|
|
if (auto *item = addresses->currentItem()) QApplication::clipboard()->setText(item->data(Qt::UserRole).toString());
|
|
});
|
|
|
|
auto *send = new QWidget; auto *sv = new QVBoxLayout(send); auto *form = new QFormLayout;
|
|
recipient = new QLineEdit; recipient->setPlaceholderText("Recipient NRB address");
|
|
amountEdit = new QLineEdit; amountEdit->setPlaceholderText("0.00000000");
|
|
feeEdit = new QLineEdit("0.00010000");
|
|
form->addRow("To", recipient); form->addRow("Amount (NRB)", amountEdit); form->addRow("Network fee (NRB)", feeEdit);
|
|
sv->addLayout(form); sendButton = new QPushButton("Review payment"); sv->addWidget(sendButton, 0, Qt::AlignLeft);
|
|
sendStatus = textLabel(""); sendStatus->setTextInteractionFlags(Qt::TextSelectableByMouse); sv->addWidget(sendStatus);
|
|
sv->addStretch(); tabs->addTab(send, "Send");
|
|
connect(sendButton, &QPushButton::clicked, this, [this] { guarded([this] { sendPayment(); }); });
|
|
|
|
auto *history = new QWidget; auto *hv = new QVBoxLayout(history);
|
|
hv->addWidget(textLabel("Recent transactions · double-click to open in the explorer"));
|
|
transactions = new QListWidget; hv->addWidget(transactions, 1); tabs->addTab(history, "Transactions");
|
|
connect(transactions, &QListWidget::itemDoubleClicked, this, [](QListWidgetItem *item) {
|
|
if (QRegularExpression("^[a-f0-9]{64}$").match(item->text()).hasMatch())
|
|
QDesktopServices::openUrl(QUrl("https://explorer.nerbus.com/tx/" + item->text()));
|
|
});
|
|
|
|
auto *settings = new QWidget; auto *wv = new QVBoxLayout(settings);
|
|
walletStatus = textLabel(""); wv->addWidget(walletStatus); unlockButton = new QPushButton;
|
|
wv->addWidget(unlockButton, 0, Qt::AlignLeft);
|
|
auto addAction = [this, wv](QString title, std::function<void()> fn) {
|
|
auto *button = new QPushButton(title); wv->addWidget(button, 0, Qt::AlignLeft);
|
|
connect(button, &QPushButton::clicked, this, [this, fn] { if (!busy) guarded(fn); });
|
|
};
|
|
addAction("Back up wallet", [this] { backup(); });
|
|
addAction("Restore backup", [this] { restore(); });
|
|
addAction("Set password (optional)", [this] { setPassword(); });
|
|
addAction("Remove password", [this] { removePassword(); });
|
|
addAction("Import private key", [this] { importKey(); });
|
|
addAction("Export selected address private key", [this] { exportKey(); });
|
|
wv->addSpacing(12);
|
|
auto *location = textLabel("Wallet file: " + store.path()); location->setTextInteractionFlags(Qt::TextSelectableByMouse); wv->addWidget(location);
|
|
wv->addWidget(textLabel("Save a new backup after adding addresses. Password protection is optional. Without it, wallet files and backups contain unencrypted private keys.\nNetwork data is provided by explorer.nerbus.com. This wallet supports transparent NRB addresses."));
|
|
wv->addStretch(); tabs->addTab(settings, "Wallet & backup");
|
|
connect(unlockButton, &QPushButton::clicked, this, [this] { guarded([this] { if (store.unlocked()) lockWallet(); else ensureUnlocked(); }); });
|
|
|
|
auto *footer = new QLabel("NERBUS / NRB 0.2.1"); layout->addWidget(footer);
|
|
setCentralWidget(root);
|
|
setStyleSheet("QMainWindow,QWidget{background:#f2ecdf;color:#211f19;font-family:'Georgia';font-size:15px;}"
|
|
"QLabel#masthead{font-size:48px;font-weight:bold;letter-spacing:5px;} QLabel#balance{font-size:38px;font-weight:bold;}"
|
|
"QLabel#address{font-family:'Consolas';font-size:21px;padding:18px 0;}"
|
|
"QTabWidget::pane{border:1px solid #29261e;padding:18px;} QTabBar::tab{padding:12px 18px;border:1px solid #29261e;}"
|
|
"QTabBar::tab:selected{background:#29261e;color:#f2ecdf;} QPushButton{background:#29261e;color:#fff8e8;padding:11px 16px;border:0;}"
|
|
"QPushButton:hover{background:#554b36;} QPushButton:disabled{background:#b8b0a0;color:#716956;}"
|
|
"QLineEdit{background:#fffaf0;border:1px solid #8c806c;padding:10px;selection-background-color:#554b36;}"
|
|
"QListWidget{background:#fffaf0;border:1px solid #8c806c;} QListWidget::item{padding:9px;border-bottom:1px solid #ded4c1;}"
|
|
"QListWidget::item:selected{background:#ded4c1;color:#211f19;}");
|
|
refreshTimer.setInterval(30000); connect(&refreshTimer, &QTimer::timeout, this, [this] { refresh(); }); refreshTimer.start();
|
|
idleTimer.setInterval(10 * 60 * 1000); connect(&idleTimer, &QTimer::timeout, this, [this] { lockWallet(); });
|
|
qApp->installEventFilter(this); updateButtons();
|
|
QTimer::singleShot(0, this, [this] {
|
|
guarded([this] {
|
|
if (store.exists() && !WalletStore::fileRequiresPassword(store.path())) {
|
|
store.unlock({}); walletOpened();
|
|
} else refresh();
|
|
});
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
QApplication app(argc, argv);
|
|
QCoreApplication::setOrganizationName("Nerbus"); QCoreApplication::setApplicationName("Nerbus Wallet");
|
|
QCoreApplication::setApplicationVersion("0.2.1");
|
|
if (sodium_init() < 0) { QMessageBox::critical(nullptr, "Nerbus Wallet", "Secure cryptography is unavailable."); return 1; }
|
|
QString directory = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/Nerbus/Wallet";
|
|
for (const QString &arg : app.arguments().mid(1)) {
|
|
if (arg.startsWith("--wallet-dir=")) directory = QFileInfo(arg.mid(13)).absoluteFilePath();
|
|
else { QMessageBox::critical(nullptr, "Nerbus Wallet", "Unsupported wallet argument."); return 1; }
|
|
}
|
|
if (!QDir().mkpath(directory)) { QMessageBox::critical(nullptr, "Nerbus Wallet", "Cannot create the wallet folder."); return 1; }
|
|
QLockFile lock(directory + "/wallet.lock"); lock.setStaleLockTime(0);
|
|
if (!lock.tryLock(0)) { QMessageBox::information(nullptr, "Nerbus Wallet", "This wallet is already open."); return 1; }
|
|
WalletWindow window(directory + "/wallet.nrb"); window.show(); return app.exec();
|
|
}
|