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:
Ricardo Guilherme Schmidt 2026-07-15 12:23:34 -03:00
parent 27547875b6
commit 051ba647fe
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
6 changed files with 420 additions and 226 deletions

View File

@ -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);
}

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,234 @@
#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.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]() {
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();
});
}

View 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;
};

View File

@ -1,6 +1,7 @@
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSettings>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QtTest>
@ -8,6 +9,7 @@
#include "FakeWalletProvider.h"
#include "LogosWalletProvider.h"
#include "WalletAccountModel.h"
#include "WalletController.h"
#include "logos_sdk.h"
namespace {
@ -53,6 +55,7 @@ private slots:
void rejectsInvalidSubmissionResponses();
void exposesStableAccountModelRoles();
void fakeProviderImplementsConsumerContract();
void controllerOwnsUiWalletFlow();
};
void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
@ -362,6 +365,55 @@ 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();
}
QTEST_GUILESS_MAIN(LogosWalletProviderTest)
#include "LogosWalletProviderTest.moc"