lez-programs/apps/shared/wallet/src/WalletController.cpp
2026-07-17 20:49:53 -03:00

365 lines
12 KiB
C++

#include "WalletController.h"
#include <utility>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPointer>
#include <QSettings>
#include <QTimer>
#include <QUrl>
#include "WalletAccountModel.h"
namespace {
const char SETTINGS_ORG[] = "Logos";
const char DISCONNECTED_KEY[] = "disconnected";
const char WALLET_HOME_ENV[] = "LEE_WALLET_HOME_DIR";
QString toLocalPath(const QString& path)
{
if (path.startsWith(QStringLiteral("file://")) || path.contains(QLatin1Char('/')))
return QUrl::fromUserInput(path).toLocalFile();
return path;
}
QString configuredSequencer(const QString& path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return {};
const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
if (!document.isObject())
return {};
return document.object().value(QStringLiteral("sequencer_addr")).toString();
}
}
WalletController::WalletController(WalletProvider& wallet,
QString settingsApplication,
QObject* parent)
: QObject(parent),
m_wallet(wallet),
m_settingsApplication(std::move(settingsApplication)),
m_accountModel(new WalletAccountModel(this)),
m_network(new QNetworkAccessManager(this)),
m_reachabilityTimer(new QTimer(this))
{
m_state.walletHome = defaultWalletHome();
m_state.configPath = defaultConfigPath();
m_state.storagePath = defaultStoragePath();
m_state.walletExists = QFileInfo::exists(defaultStoragePath());
m_state.sequencerAddress = configuredSequencer(defaultConfigPath());
m_reachabilityTimer->setInterval(10000);
connect(m_reachabilityTimer, &QTimer::timeout,
this, &WalletController::checkReachability);
}
WalletController::~WalletController() = default;
QString WalletController::defaultWalletHome()
{
const QByteArray override = qgetenv(WALLET_HOME_ENV);
if (!override.isEmpty())
return QString::fromLocal8Bit(override);
return QDir::homePath() + QStringLiteral("/.lee/wallet");
}
QString WalletController::defaultConfigPath() const
{
return m_state.walletHome + QStringLiteral("/wallet_config.json");
}
QString WalletController::defaultStoragePath() const
{
return m_state.walletHome + QStringLiteral("/storage.json");
}
void WalletController::start()
{
if (m_started)
return;
m_started = true;
QTimer::singleShot(0, this, &WalletController::openOnStartup);
}
void WalletController::openOnStartup()
{
if (QSettings(SETTINGS_ORG, m_settingsApplication)
.value(DISCONNECTED_KEY, false).toBool()) {
return;
}
const QString config = defaultConfigPath();
const QString storage = defaultStoragePath();
beginOpen(config, storage);
}
bool WalletController::beginOpen(const QString& config, const QString& storage)
{
if (m_state.syncStatus == QStringLiteral("opening")
|| m_state.syncStatus == QStringLiteral("syncing")) {
return false;
}
const quint64 generation = ++m_operationGeneration;
m_state.configPath = config;
m_state.storagePath = storage;
m_state.syncStatus = QStringLiteral("opening");
m_state.syncError.clear();
const QString endpoint = configuredSequencer(config);
if (!endpoint.isEmpty())
m_state.sequencerAddress = endpoint;
emit stateChanged();
QTimer::singleShot(0, this, [this, generation]() {
if (generation == m_operationGeneration
&& m_state.syncStatus == QStringLiteral("opening")) {
m_state.syncStatus = QStringLiteral("syncing");
emit stateChanged();
}
});
const QPointer<WalletController> guard(this);
m_wallet.connectAsync({ config, storage },
[guard, generation, config, storage](WalletSession session) {
if (!guard || generation != guard->m_operationGeneration)
return;
if (session.failure == WalletFailure::WalletMissing) {
guard->m_state.syncStatus = QStringLiteral("closed");
guard->m_state.walletExists = false;
emit guard->stateChanged();
return;
}
if (!session.ok()) {
qWarning() << "WalletController: wallet connection failed"
<< walletFailureCode(session.failure);
guard->m_state.syncStatus = QStringLiteral("error");
guard->m_state.syncError = walletFailureCode(session.failure);
emit guard->stateChanged();
return;
}
guard->m_state.configPath = config;
guard->m_state.storagePath = storage;
guard->m_state.walletExists = QFileInfo::exists(storage) || session.adopted;
guard->m_state.isWalletOpen = true;
guard->m_state.syncStatus = QStringLiteral("ready");
guard->applySnapshot(session.snapshot);
});
return true;
}
QString WalletController::createDefaultWallet(const QString& password)
{
return createWallet(defaultConfigPath(), defaultStoragePath(), password);
}
QString WalletController::createWallet(const QString& configPath,
const QString& storagePath,
const QString& password)
{
const quint64 generation = ++m_operationGeneration;
const QString config = toLocalPath(configPath);
const QString storage = toLocalPath(storagePath);
const WalletCreation creation = m_wallet.createWallet(
{ config, storage }, password);
if (creation.mnemonic.isEmpty()) {
qWarning() << "WalletController: wallet creation failed"
<< walletFailureCode(creation.failure);
return {};
}
stopReachability();
m_state.configPath = config;
m_state.storagePath = storage;
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
if (!creation.ok()) {
qWarning() << "WalletController: wallet creation failed"
<< walletFailureCode(creation.failure);
m_state.walletExists = QFileInfo::exists(storage);
m_state.isWalletOpen = false;
m_state.syncStatus = QStringLiteral("error");
m_state.syncError = walletFailureCode(creation.failure);
emit stateChanged();
return creation.mnemonic;
}
m_state.walletExists = true;
m_state.isWalletOpen = true;
m_state.syncStatus = QStringLiteral("syncing");
m_state.syncError.clear();
m_accountModel->replaceAccounts({});
emit stateChanged();
const QPointer<WalletController> guard(this);
QTimer::singleShot(0, this, [guard, generation]() {
if (!guard || generation != guard->m_operationGeneration)
return;
guard->m_wallet.snapshotAsync(true,
[guard, generation](WalletSnapshot snapshot) {
if (!guard || generation != guard->m_operationGeneration)
return;
if (snapshot.ok()) {
guard->m_state.syncStatus = QStringLiteral("ready");
guard->applySnapshot(snapshot);
return;
}
qWarning() << "WalletController: initial wallet sync failed"
<< walletFailureCode(snapshot.failure);
guard->m_state.syncStatus = QStringLiteral("error");
guard->m_state.syncError = walletFailureCode(snapshot.failure);
emit guard->stateChanged();
});
});
return creation.mnemonic;
}
bool WalletController::open()
{
const QString config = m_state.configPath.isEmpty()
? defaultConfigPath() : m_state.configPath;
const QString storage = m_state.storagePath.isEmpty()
? defaultStoragePath() : m_state.storagePath;
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
return beginOpen(config, storage);
}
void WalletController::disconnect()
{
++m_operationGeneration;
stopReachability();
m_wallet.disconnect();
m_state.isWalletOpen = false;
m_state.syncStatus = QStringLiteral("closed");
m_state.syncError.clear();
m_accountModel->replaceAccounts({});
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, true);
emit stateChanged();
}
QString WalletController::createAccount(bool isPublic)
{
const WalletAccountCreation creation = m_wallet.createAccount(isPublic);
if (!creation.ok()) {
qWarning() << "WalletController: account creation failed"
<< walletFailureCode(creation.failure);
return {};
}
if (creation.snapshot.ok()) {
applySnapshot(creation.snapshot);
} else {
qWarning() << "WalletController: account refresh failed"
<< walletFailureCode(creation.snapshot.failure);
}
return creation.accountId;
}
void WalletController::refresh()
{
if (!m_state.isWalletOpen || m_state.syncStatus == QStringLiteral("syncing"))
return;
const quint64 generation = ++m_operationGeneration;
m_state.syncStatus = QStringLiteral("syncing");
m_state.syncError.clear();
emit stateChanged();
const QPointer<WalletController> guard(this);
m_wallet.snapshotAsync(true, [guard, generation](WalletSnapshot next) {
if (!guard || generation != guard->m_operationGeneration)
return;
if (next.ok()) {
guard->m_state.syncStatus = QStringLiteral("ready");
guard->applySnapshot(next);
} else {
qWarning() << "WalletController: wallet refresh failed"
<< walletFailureCode(next.failure);
guard->m_state.syncStatus = QStringLiteral("error");
guard->m_state.syncError = walletFailureCode(next.failure);
emit guard->stateChanged();
}
});
}
QString WalletController::balance(const QString& accountId, bool isPublic)
{
const WalletSnapshot current = m_wallet.snapshot();
for (const WalletAccount& account : current.accounts) {
if (account.address == accountId && account.isPublic == isPublic)
return account.balance;
}
return {};
}
void WalletController::applySnapshot(const WalletSnapshot& snapshot)
{
m_accountModel->replaceAccounts(snapshot.accounts);
m_state.lastSyncedBlock = static_cast<int>(snapshot.lastSyncedBlock);
m_state.currentBlockHeight = static_cast<int>(snapshot.currentBlockHeight);
if (!snapshot.sequencerAddress.isEmpty())
m_state.sequencerAddress = snapshot.sequencerAddress;
emit stateChanged();
if (!m_reachabilityTimer->isActive())
m_reachabilityTimer->start();
checkReachability();
}
void WalletController::stopReachability()
{
m_reachabilityTimer->stop();
++m_reachabilityGeneration;
if (m_reachabilityReply) {
QNetworkReply* reply = m_reachabilityReply;
m_reachabilityReply = nullptr;
m_reachabilityEndpoint.clear();
reply->abort();
}
}
void WalletController::checkReachability()
{
if (!m_state.isWalletOpen || m_state.sequencerAddress.isEmpty())
return;
const QString endpoint = m_state.sequencerAddress;
if (m_reachabilityReply && endpoint == m_reachabilityEndpoint)
return;
const quint64 generation = ++m_reachabilityGeneration;
if (m_reachabilityReply)
m_reachabilityReply->abort();
QNetworkRequest request{QUrl(endpoint)};
request.setTransferTimeout(4000);
QNetworkReply* reply = m_network->get(request);
m_reachabilityReply = reply;
m_reachabilityEndpoint = endpoint;
connect(reply, &QNetworkReply::finished, this,
[this, reply, generation, endpoint]() {
if (m_reachabilityReply == reply) {
m_reachabilityReply = nullptr;
m_reachabilityEndpoint.clear();
}
if (!m_state.isWalletOpen
|| generation != m_reachabilityGeneration
|| endpoint != m_state.sequencerAddress) {
reply->deleteLater();
return;
}
const bool receivedHttp =
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid();
const bool reachable = receivedHttp || reply->error() == QNetworkReply::NoError;
if (m_state.sequencerReachable != reachable) {
m_state.sequencerReachable = reachable;
emit stateChanged();
}
reply->deleteLater();
});
}