mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-17 04:19:35 +00:00
refactor(wallet): centralize UI wallet coordination
Move wallet lifecycle, account-model synchronization, settings, and reachability behind a composition-based WalletController. Keep AmmUiBackend as the QtRO forwarding adapter while retaining direct WalletProvider ownership for program-specific reads and transaction submission. Adopt the thin-consumer direction demonstrated by PR #230 without coupling the shared module to generated QtRO base classes. Refs #227 Refs #230
This commit is contained in:
parent
ae52613279
commit
049ccdf6a7
@ -1,248 +1,86 @@
|
||||
#include "AmmUiBackend.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "LogosWalletProvider.h"
|
||||
#include "WalletController.h"
|
||||
#include "logos_api.h"
|
||||
|
||||
namespace {
|
||||
const char SETTINGS_ORG[] = "Logos";
|
||||
const char SETTINGS_APP[] = "AmmUI";
|
||||
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 AmmUiBackend::defaultWalletHome()
|
||||
{
|
||||
const QByteArray override = qgetenv(WALLET_HOME_ENV);
|
||||
if (!override.isEmpty())
|
||||
return QString::fromLocal8Bit(override);
|
||||
return QDir::homePath() + QStringLiteral("/.lee/wallet");
|
||||
}
|
||||
|
||||
QString AmmUiBackend::defaultConfigPath() const
|
||||
{
|
||||
return defaultWalletHome() + QStringLiteral("/wallet_config.json");
|
||||
}
|
||||
|
||||
QString AmmUiBackend::defaultStoragePath() const
|
||||
{
|
||||
return defaultWalletHome() + QStringLiteral("/storage.json");
|
||||
}
|
||||
|
||||
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
: AmmUiBackendSimpleSource(parent),
|
||||
m_accountModel(new WalletAccountModel(this)),
|
||||
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
|
||||
m_wallet(std::make_unique<LogosWalletProvider>(m_logosAPI)),
|
||||
m_net(new QNetworkAccessManager(this)),
|
||||
m_reachabilityTimer(new QTimer(this))
|
||||
m_walletController(std::make_unique<WalletController>(
|
||||
*m_wallet, QStringLiteral("AmmUI")))
|
||||
{
|
||||
setIsWalletOpen(false);
|
||||
setLastSyncedBlock(0);
|
||||
setCurrentBlockHeight(0);
|
||||
setWalletHome(defaultWalletHome());
|
||||
setSequencerReachable(true);
|
||||
setWalletExists(QFileInfo::exists(defaultStoragePath()));
|
||||
|
||||
m_reachabilityTimer->setInterval(10000);
|
||||
connect(m_reachabilityTimer, &QTimer::timeout,
|
||||
this, &AmmUiBackend::checkReachability);
|
||||
m_reachabilityTimer->start();
|
||||
|
||||
QTimer::singleShot(0, this, &AmmUiBackend::openOrAdoptWallet);
|
||||
connect(m_walletController.get(), &WalletController::stateChanged,
|
||||
this, &AmmUiBackend::syncWalletState);
|
||||
syncWalletState();
|
||||
m_walletController->start();
|
||||
}
|
||||
|
||||
AmmUiBackend::~AmmUiBackend() = default;
|
||||
|
||||
void AmmUiBackend::openOrAdoptWallet()
|
||||
WalletAccountModel* AmmUiBackend::accountModel() const
|
||||
{
|
||||
if (QSettings(SETTINGS_ORG, SETTINGS_APP).value(DISCONNECTED_KEY, false).toBool())
|
||||
return;
|
||||
return m_walletController->accountModel();
|
||||
}
|
||||
|
||||
const QString config = defaultConfigPath();
|
||||
const QString storage = defaultStoragePath();
|
||||
const WalletSession session = m_wallet->connect({ config, storage });
|
||||
if (session.failure == WalletFailure::WalletMissing)
|
||||
return;
|
||||
if (!session.ok()) {
|
||||
qWarning() << "AmmUiBackend: wallet connection failed"
|
||||
<< walletFailureCode(session.failure);
|
||||
return;
|
||||
}
|
||||
QString AmmUiBackend::createAccountPublic()
|
||||
{
|
||||
return m_walletController->createAccount(true);
|
||||
}
|
||||
|
||||
persistConfigPath(config);
|
||||
persistStoragePath(storage);
|
||||
setWalletExists(QFileInfo::exists(storage) || session.adopted);
|
||||
setIsWalletOpen(true);
|
||||
applySnapshot(session.snapshot);
|
||||
QString AmmUiBackend::createAccountPrivate()
|
||||
{
|
||||
return m_walletController->createAccount(false);
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshAccounts()
|
||||
{
|
||||
m_walletController->refresh();
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshBalances()
|
||||
{
|
||||
m_walletController->refresh();
|
||||
}
|
||||
|
||||
QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
|
||||
{
|
||||
return m_walletController->balance(accountIdHex, isPublic);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::createNewDefault(QString password)
|
||||
{
|
||||
return createNew(defaultConfigPath(), defaultStoragePath(), password);
|
||||
return m_walletController->createDefaultWallet(password);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::createNew(QString configPath,
|
||||
QString storagePath,
|
||||
QString password)
|
||||
{
|
||||
const QString config = toLocalPath(configPath);
|
||||
const QString storage = toLocalPath(storagePath);
|
||||
const WalletCreation creation = m_wallet->createWallet(
|
||||
{ config, storage }, password);
|
||||
if (creation.mnemonic.isEmpty()) {
|
||||
qWarning() << "AmmUiBackend: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return {};
|
||||
}
|
||||
|
||||
persistConfigPath(config);
|
||||
persistStoragePath(storage);
|
||||
setWalletExists(true);
|
||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "AmmUiBackend: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return creation.mnemonic;
|
||||
}
|
||||
|
||||
setIsWalletOpen(true);
|
||||
applySnapshot(creation.snapshot);
|
||||
return creation.mnemonic;
|
||||
return m_walletController->createWallet(configPath, storagePath, password);
|
||||
}
|
||||
|
||||
bool AmmUiBackend::openExisting()
|
||||
{
|
||||
const QString config = configPath().isEmpty() ? defaultConfigPath() : configPath();
|
||||
const QString storage = storagePath().isEmpty() ? defaultStoragePath() : storagePath();
|
||||
const WalletSession session = m_wallet->connect({ config, storage });
|
||||
if (!session.ok()) {
|
||||
qWarning() << "AmmUiBackend: wallet open failed"
|
||||
<< walletFailureCode(session.failure);
|
||||
return false;
|
||||
}
|
||||
|
||||
persistConfigPath(config);
|
||||
persistStoragePath(storage);
|
||||
setWalletExists(true);
|
||||
setIsWalletOpen(true);
|
||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, false);
|
||||
applySnapshot(session.snapshot);
|
||||
return true;
|
||||
return m_walletController->open();
|
||||
}
|
||||
|
||||
void AmmUiBackend::disconnectWallet()
|
||||
{
|
||||
m_wallet->disconnect();
|
||||
setIsWalletOpen(false);
|
||||
m_accountModel->replaceAccounts({});
|
||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, true);
|
||||
m_walletController->disconnect();
|
||||
}
|
||||
|
||||
QString AmmUiBackend::createAccountPublic()
|
||||
void AmmUiBackend::syncWalletState()
|
||||
{
|
||||
const WalletAccountCreation creation = m_wallet->createAccount(true);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "AmmUiBackend: public account creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return {};
|
||||
}
|
||||
if (creation.snapshot.ok())
|
||||
applySnapshot(creation.snapshot);
|
||||
else
|
||||
qWarning() << "AmmUiBackend: account refresh failed"
|
||||
<< walletFailureCode(creation.snapshot.failure);
|
||||
return creation.accountId;
|
||||
}
|
||||
|
||||
QString AmmUiBackend::createAccountPrivate()
|
||||
{
|
||||
const WalletAccountCreation creation = m_wallet->createAccount(false);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "AmmUiBackend: private account creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return {};
|
||||
}
|
||||
if (creation.snapshot.ok())
|
||||
applySnapshot(creation.snapshot);
|
||||
else
|
||||
qWarning() << "AmmUiBackend: account refresh failed"
|
||||
<< walletFailureCode(creation.snapshot.failure);
|
||||
return creation.accountId;
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshAccounts()
|
||||
{
|
||||
const WalletSnapshot next = m_wallet->snapshot(true);
|
||||
if (next.ok())
|
||||
applySnapshot(next);
|
||||
else
|
||||
qWarning() << "AmmUiBackend: wallet refresh failed"
|
||||
<< walletFailureCode(next.failure);
|
||||
}
|
||||
|
||||
void AmmUiBackend::refreshBalances()
|
||||
{
|
||||
refreshAccounts();
|
||||
}
|
||||
|
||||
QString AmmUiBackend::getBalance(QString accountIdHex, bool isPublic)
|
||||
{
|
||||
const WalletSnapshot current = m_wallet->snapshot();
|
||||
for (const WalletAccount& account : current.accounts) {
|
||||
if (account.address == accountIdHex && account.isPublic == isPublic)
|
||||
return account.balance;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void AmmUiBackend::applySnapshot(const WalletSnapshot& snapshot)
|
||||
{
|
||||
m_accountModel->replaceAccounts(snapshot.accounts);
|
||||
setLastSyncedBlock(static_cast<int>(snapshot.lastSyncedBlock));
|
||||
setCurrentBlockHeight(static_cast<int>(snapshot.currentBlockHeight));
|
||||
setSequencerAddr(snapshot.sequencerAddress);
|
||||
checkReachability();
|
||||
}
|
||||
|
||||
void AmmUiBackend::checkReachability()
|
||||
{
|
||||
if (sequencerAddr().isEmpty())
|
||||
return;
|
||||
|
||||
QNetworkRequest request{QUrl(sequencerAddr())};
|
||||
request.setTransferTimeout(4000);
|
||||
QNetworkReply* reply = m_net->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
const bool receivedHttp =
|
||||
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid();
|
||||
setSequencerReachable(receivedHttp || reply->error() == QNetworkReply::NoError);
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void AmmUiBackend::persistConfigPath(const QString& path)
|
||||
{
|
||||
setConfigPath(toLocalPath(path));
|
||||
}
|
||||
|
||||
void AmmUiBackend::persistStoragePath(const QString& path)
|
||||
{
|
||||
setStoragePath(toLocalPath(path));
|
||||
const WalletUiState& state = m_walletController->state();
|
||||
setIsWalletOpen(state.isWalletOpen);
|
||||
setWalletExists(state.walletExists);
|
||||
setConfigPath(state.configPath);
|
||||
setStoragePath(state.storagePath);
|
||||
setWalletHome(state.walletHome);
|
||||
setLastSyncedBlock(state.lastSyncedBlock);
|
||||
setCurrentBlockHeight(state.currentBlockHeight);
|
||||
setSequencerAddr(state.sequencerAddress);
|
||||
setSequencerReachable(state.sequencerReachable);
|
||||
}
|
||||
|
||||
@ -11,8 +11,7 @@
|
||||
|
||||
class LogosAPI;
|
||||
class LogosWalletProvider;
|
||||
class QNetworkAccessManager;
|
||||
class QTimer;
|
||||
class WalletController;
|
||||
|
||||
class AmmUiBackend : public AmmUiBackendSimpleSource {
|
||||
Q_OBJECT
|
||||
@ -22,7 +21,7 @@ public:
|
||||
explicit AmmUiBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
|
||||
~AmmUiBackend() override;
|
||||
|
||||
WalletAccountModel* accountModel() const { return m_accountModel; }
|
||||
WalletAccountModel* accountModel() const;
|
||||
|
||||
public slots:
|
||||
QString createAccountPublic() override;
|
||||
@ -36,21 +35,11 @@ public slots:
|
||||
void disconnectWallet() override;
|
||||
|
||||
private:
|
||||
static QString defaultWalletHome();
|
||||
QString defaultConfigPath() const;
|
||||
QString defaultStoragePath() const;
|
||||
void syncWalletState();
|
||||
|
||||
void persistConfigPath(const QString& path);
|
||||
void persistStoragePath(const QString& path);
|
||||
void openOrAdoptWallet();
|
||||
void applySnapshot(const WalletSnapshot& snapshot);
|
||||
void checkReachability();
|
||||
|
||||
WalletAccountModel* m_accountModel;
|
||||
LogosAPI* m_logosAPI;
|
||||
std::unique_ptr<LogosWalletProvider> m_wallet;
|
||||
QNetworkAccessManager* m_net;
|
||||
QTimer* m_reachabilityTimer;
|
||||
std::unique_ptr<WalletController> m_walletController;
|
||||
};
|
||||
|
||||
#endif // AMM_UI_BACKEND_H
|
||||
|
||||
@ -18,6 +18,9 @@ if(LOGOS_WALLET_BUILD_ACCESS
|
||||
endif()
|
||||
|
||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Core)
|
||||
if(LOGOS_WALLET_BUILD_ACCESS OR BUILD_TESTING)
|
||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Network)
|
||||
endif()
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
if(LOGOS_WALLET_BUILD_ACCESS)
|
||||
@ -28,6 +31,8 @@ if(LOGOS_WALLET_BUILD_ACCESS)
|
||||
src/LogosWalletProvider.cpp
|
||||
src/WalletAccountModel.h
|
||||
src/WalletAccountModel.cpp
|
||||
src/WalletController.h
|
||||
src/WalletController.cpp
|
||||
)
|
||||
set_target_properties(logos_wallet_access PROPERTIES
|
||||
AUTOMOC ON
|
||||
@ -41,7 +46,10 @@ if(LOGOS_WALLET_BUILD_ACCESS)
|
||||
"${LOGOS_WALLET_GENERATED_DIR}"
|
||||
"${LOGOS_WALLET_GENERATED_DIR}/include"
|
||||
)
|
||||
target_link_libraries(logos_wallet_access PUBLIC Qt6::Core)
|
||||
target_link_libraries(logos_wallet_access
|
||||
PUBLIC Qt6::Core
|
||||
PRIVATE Qt6::Network
|
||||
)
|
||||
endif()
|
||||
|
||||
if(LOGOS_WALLET_BUILD_QML)
|
||||
@ -127,6 +135,8 @@ if(BUILD_TESTING)
|
||||
src/LogosWalletProvider.cpp
|
||||
src/WalletAccountModel.cpp
|
||||
src/WalletAccountModel.h
|
||||
src/WalletController.cpp
|
||||
src/WalletController.h
|
||||
)
|
||||
set_target_properties(logos_wallet_access_test PROPERTIES AUTOMOC ON)
|
||||
target_compile_features(logos_wallet_access_test PRIVATE cxx_std_17)
|
||||
@ -135,7 +145,11 @@ if(BUILD_TESTING)
|
||||
tests/support
|
||||
src
|
||||
)
|
||||
target_link_libraries(logos_wallet_access_test PRIVATE Qt6::Core Qt6::Test)
|
||||
target_link_libraries(logos_wallet_access_test PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Network
|
||||
Qt6::Test
|
||||
)
|
||||
add_test(NAME logos_wallet_access COMMAND logos_wallet_access_test)
|
||||
|
||||
if(LOGOS_WALLET_BUILD_QML)
|
||||
|
||||
238
apps/shared/wallet/src/WalletController.cpp
Normal file
238
apps/shared/wallet/src/WalletController.cpp
Normal file
@ -0,0 +1,238 @@
|
||||
#include "WalletController.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
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.walletExists = QFileInfo::exists(defaultStoragePath());
|
||||
|
||||
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;
|
||||
m_reachabilityTimer->start();
|
||||
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();
|
||||
const WalletSession session = m_wallet.connect({ config, storage });
|
||||
if (session.failure == WalletFailure::WalletMissing)
|
||||
return;
|
||||
if (!session.ok()) {
|
||||
qWarning() << "WalletController: wallet connection failed"
|
||||
<< walletFailureCode(session.failure);
|
||||
return;
|
||||
}
|
||||
|
||||
m_state.configPath = config;
|
||||
m_state.storagePath = storage;
|
||||
m_state.walletExists = QFileInfo::exists(storage) || session.adopted;
|
||||
m_state.isWalletOpen = true;
|
||||
applySnapshot(session.snapshot);
|
||||
}
|
||||
|
||||
QString WalletController::createDefaultWallet(const QString& password)
|
||||
{
|
||||
return createWallet(defaultConfigPath(), defaultStoragePath(), password);
|
||||
}
|
||||
|
||||
QString WalletController::createWallet(const QString& configPath,
|
||||
const QString& storagePath,
|
||||
const QString& password)
|
||||
{
|
||||
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 {};
|
||||
}
|
||||
|
||||
m_state.configPath = config;
|
||||
m_state.storagePath = storage;
|
||||
m_state.walletExists = true;
|
||||
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "WalletController: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
emit stateChanged();
|
||||
return creation.mnemonic;
|
||||
}
|
||||
|
||||
m_state.isWalletOpen = true;
|
||||
applySnapshot(creation.snapshot);
|
||||
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;
|
||||
const WalletSession session = m_wallet.connect({ config, storage });
|
||||
if (!session.ok()) {
|
||||
qWarning() << "WalletController: wallet open failed"
|
||||
<< walletFailureCode(session.failure);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state.configPath = config;
|
||||
m_state.storagePath = storage;
|
||||
m_state.walletExists = true;
|
||||
m_state.isWalletOpen = true;
|
||||
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
|
||||
applySnapshot(session.snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
void WalletController::disconnect()
|
||||
{
|
||||
m_wallet.disconnect();
|
||||
m_state.isWalletOpen = false;
|
||||
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()
|
||||
{
|
||||
const WalletSnapshot next = m_wallet.snapshot(true);
|
||||
if (next.ok()) {
|
||||
applySnapshot(next);
|
||||
} else {
|
||||
qWarning() << "WalletController: wallet refresh failed"
|
||||
<< walletFailureCode(next.failure);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
m_state.sequencerAddress = snapshot.sequencerAddress;
|
||||
emit stateChanged();
|
||||
checkReachability();
|
||||
}
|
||||
|
||||
void WalletController::checkReachability()
|
||||
{
|
||||
if (!m_state.isWalletOpen || m_state.sequencerAddress.isEmpty())
|
||||
return;
|
||||
|
||||
QNetworkRequest request{QUrl(m_state.sequencerAddress)};
|
||||
request.setTransferTimeout(4000);
|
||||
QNetworkReply* reply = m_network->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
if (!m_state.isWalletOpen) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
67
apps/shared/wallet/src/WalletController.h
Normal file
67
apps/shared/wallet/src/WalletController.h
Normal file
@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "WalletProvider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QTimer;
|
||||
class WalletAccountModel;
|
||||
|
||||
struct WalletUiState {
|
||||
bool isWalletOpen = false;
|
||||
bool walletExists = false;
|
||||
QString configPath;
|
||||
QString storagePath;
|
||||
QString walletHome;
|
||||
int lastSyncedBlock = 0;
|
||||
int currentBlockHeight = 0;
|
||||
QString sequencerAddress;
|
||||
bool sequencerReachable = true;
|
||||
};
|
||||
|
||||
class WalletController final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// The provider must outlive the controller.
|
||||
explicit WalletController(WalletProvider& wallet,
|
||||
QString settingsApplication,
|
||||
QObject* parent = nullptr);
|
||||
~WalletController() override;
|
||||
|
||||
WalletAccountModel* accountModel() const { return m_accountModel; }
|
||||
const WalletUiState& state() const { return m_state; }
|
||||
|
||||
void start();
|
||||
QString createAccount(bool isPublic);
|
||||
void refresh();
|
||||
QString balance(const QString& accountId, bool isPublic);
|
||||
QString createDefaultWallet(const QString& password);
|
||||
QString createWallet(const QString& configPath,
|
||||
const QString& storagePath,
|
||||
const QString& password);
|
||||
bool open();
|
||||
void disconnect();
|
||||
|
||||
signals:
|
||||
void stateChanged();
|
||||
|
||||
private:
|
||||
static QString defaultWalletHome();
|
||||
QString defaultConfigPath() const;
|
||||
QString defaultStoragePath() const;
|
||||
|
||||
void openOnStartup();
|
||||
void applySnapshot(const WalletSnapshot& snapshot);
|
||||
void checkReachability();
|
||||
|
||||
WalletProvider& m_wallet;
|
||||
QString m_settingsApplication;
|
||||
WalletUiState m_state;
|
||||
WalletAccountModel* m_accountModel;
|
||||
QNetworkAccessManager* m_network;
|
||||
QTimer* m_reachabilityTimer;
|
||||
bool m_started = false;
|
||||
};
|
||||
@ -1,13 +1,17 @@
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QSettings>
|
||||
#include <QSignalSpy>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTimer>
|
||||
#include <QtTest>
|
||||
|
||||
#include "FakeWalletProvider.h"
|
||||
#include "LogosWalletProvider.h"
|
||||
#include "WalletAccountModel.h"
|
||||
#include "WalletController.h"
|
||||
#include "logos_sdk.h"
|
||||
|
||||
namespace {
|
||||
@ -53,6 +57,8 @@ private slots:
|
||||
void rejectsInvalidSubmissionResponses();
|
||||
void exposesStableAccountModelRoles();
|
||||
void fakeProviderImplementsConsumerContract();
|
||||
void controllerOwnsUiWalletFlow();
|
||||
void controllerStopsReachabilityChecksAfterDisconnect();
|
||||
};
|
||||
|
||||
void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
|
||||
@ -355,6 +361,8 @@ void LogosWalletProviderTest::fakeProviderImplementsConsumerContract()
|
||||
|
||||
QCOMPARE(provider.snapshot(true).accounts.size(), 1);
|
||||
QVERIFY(provider.lastForceRefresh);
|
||||
QCOMPARE(provider.readPublicAccount(ACCOUNT_B).accountId, ACCOUNT_B);
|
||||
QCOMPARE(provider.readCalls, 1);
|
||||
WalletTransaction transaction { PROGRAM_ID, { ACCOUNT_A }, { true }, { 9 } };
|
||||
QVERIFY(provider.submitPublicTransaction(transaction).accepted());
|
||||
QCOMPARE(provider.lastTransaction.instruction, transaction.instruction);
|
||||
@ -362,6 +370,83 @@ void LogosWalletProviderTest::fakeProviderImplementsConsumerContract()
|
||||
QCOMPARE(provider.disconnectCalls, 1);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerOwnsUiWalletFlow()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletControllerTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.connectResult.snapshot.accounts = {
|
||||
{ ACCOUNT_A, QStringLiteral("5"), true },
|
||||
};
|
||||
provider.connectResult.snapshot.lastSyncedBlock = 7;
|
||||
provider.connectResult.snapshot.currentBlockHeight = 8;
|
||||
|
||||
WalletController controller(provider, settingsApplication);
|
||||
QSignalSpy stateChanged(&controller, &WalletController::stateChanged);
|
||||
|
||||
QVERIFY(controller.open());
|
||||
QCOMPARE(provider.connectCalls, 1);
|
||||
QVERIFY(controller.state().isWalletOpen);
|
||||
QVERIFY(controller.state().walletExists);
|
||||
QCOMPARE(controller.state().lastSyncedBlock, 7);
|
||||
QCOMPARE(controller.state().currentBlockHeight, 8);
|
||||
QCOMPARE(controller.accountModel()->count(), 1);
|
||||
|
||||
provider.snapshotResult.accounts = {
|
||||
{ ACCOUNT_A, QStringLiteral("9"), true },
|
||||
};
|
||||
controller.refresh();
|
||||
QVERIFY(provider.lastForceRefresh);
|
||||
QCOMPARE(controller.balance(ACCOUNT_A, true), QStringLiteral("9"));
|
||||
|
||||
provider.createAccountResult.accountId = ACCOUNT_B;
|
||||
provider.createAccountResult.snapshot.accounts = {
|
||||
{ ACCOUNT_A, QStringLiteral("9"), true },
|
||||
{ ACCOUNT_B, QStringLiteral("3"), false },
|
||||
};
|
||||
QCOMPARE(controller.createAccount(false), ACCOUNT_B);
|
||||
QVERIFY(!provider.lastAccountWasPublic);
|
||||
QCOMPARE(controller.accountModel()->count(), 2);
|
||||
|
||||
controller.disconnect();
|
||||
QCOMPARE(provider.disconnectCalls, 1);
|
||||
QVERIFY(!controller.state().isWalletOpen);
|
||||
QCOMPARE(controller.accountModel()->count(), 0);
|
||||
QVERIFY(stateChanged.count() >= 4);
|
||||
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerStopsReachabilityChecksAfterDisconnect()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletReachabilityTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.connectResult.snapshot.sequencerAddress = QStringLiteral("http://127.0.0.1:1");
|
||||
WalletController controller(provider, settingsApplication);
|
||||
auto* network = controller.findChild<QNetworkAccessManager*>();
|
||||
QVERIFY(network);
|
||||
QSignalSpy finished(network, &QNetworkAccessManager::finished);
|
||||
|
||||
QVERIFY(controller.open());
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!finished.isEmpty(), 1000);
|
||||
controller.disconnect();
|
||||
finished.clear();
|
||||
|
||||
auto* timer = controller.findChild<QTimer*>();
|
||||
QVERIFY(timer);
|
||||
timer->setInterval(1);
|
||||
controller.start();
|
||||
QTest::qWait(50);
|
||||
QCOMPARE(finished.count(), 0);
|
||||
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(LogosWalletProviderTest)
|
||||
|
||||
#include "LogosWalletProviderTest.moc"
|
||||
|
||||
@ -55,10 +55,12 @@ public:
|
||||
return createAccountResult;
|
||||
}
|
||||
|
||||
WalletAccountRead readPublicAccount(const QString&) const override
|
||||
WalletAccountRead readPublicAccount(const QString& accountId) const override
|
||||
{
|
||||
++readCalls;
|
||||
return readResult;
|
||||
WalletAccountRead result = readResult;
|
||||
result.accountId = accountId;
|
||||
return result;
|
||||
}
|
||||
|
||||
WalletSubmission submitPublicTransaction(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user