mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-19 13:29:56 +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 "AmmUiBackend.h"
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QNetworkAccessManager>
|
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QNetworkRequest>
|
|
||||||
#include <QSettings>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QUrl>
|
|
||||||
|
|
||||||
#include "LogosWalletProvider.h"
|
#include "LogosWalletProvider.h"
|
||||||
|
#include "WalletController.h"
|
||||||
#include "logos_api.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)
|
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
|
||||||
: AmmUiBackendSimpleSource(parent),
|
: AmmUiBackendSimpleSource(parent),
|
||||||
m_accountModel(new WalletAccountModel(this)),
|
|
||||||
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
|
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
|
||||||
m_wallet(std::make_unique<LogosWalletProvider>(m_logosAPI)),
|
m_wallet(std::make_unique<LogosWalletProvider>(m_logosAPI)),
|
||||||
m_net(new QNetworkAccessManager(this)),
|
m_walletController(std::make_unique<WalletController>(
|
||||||
m_reachabilityTimer(new QTimer(this))
|
*m_wallet, QStringLiteral("AmmUI")))
|
||||||
{
|
{
|
||||||
setIsWalletOpen(false);
|
connect(m_walletController.get(), &WalletController::stateChanged,
|
||||||
setLastSyncedBlock(0);
|
this, &AmmUiBackend::syncWalletState);
|
||||||
setCurrentBlockHeight(0);
|
syncWalletState();
|
||||||
setWalletHome(defaultWalletHome());
|
m_walletController->start();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AmmUiBackend::~AmmUiBackend() = default;
|
AmmUiBackend::~AmmUiBackend() = default;
|
||||||
|
|
||||||
void AmmUiBackend::openOrAdoptWallet()
|
WalletAccountModel* AmmUiBackend::accountModel() const
|
||||||
{
|
{
|
||||||
if (QSettings(SETTINGS_ORG, SETTINGS_APP).value(DISCONNECTED_KEY, false).toBool())
|
return m_walletController->accountModel();
|
||||||
return;
|
}
|
||||||
|
|
||||||
const QString config = defaultConfigPath();
|
QString AmmUiBackend::createAccountPublic()
|
||||||
const QString storage = defaultStoragePath();
|
{
|
||||||
const WalletSession session = m_wallet->connect({ config, storage });
|
return m_walletController->createAccount(true);
|
||||||
if (session.failure == WalletFailure::WalletMissing)
|
}
|
||||||
return;
|
|
||||||
if (!session.ok()) {
|
|
||||||
qWarning() << "AmmUiBackend: wallet connection failed"
|
|
||||||
<< walletFailureCode(session.failure);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
persistConfigPath(config);
|
QString AmmUiBackend::createAccountPrivate()
|
||||||
persistStoragePath(storage);
|
{
|
||||||
setWalletExists(QFileInfo::exists(storage) || session.adopted);
|
return m_walletController->createAccount(false);
|
||||||
setIsWalletOpen(true);
|
}
|
||||||
applySnapshot(session.snapshot);
|
|
||||||
|
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)
|
QString AmmUiBackend::createNewDefault(QString password)
|
||||||
{
|
{
|
||||||
return createNew(defaultConfigPath(), defaultStoragePath(), password);
|
return m_walletController->createDefaultWallet(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString AmmUiBackend::createNew(QString configPath,
|
QString AmmUiBackend::createNew(QString configPath,
|
||||||
QString storagePath,
|
QString storagePath,
|
||||||
QString password)
|
QString password)
|
||||||
{
|
{
|
||||||
const QString config = toLocalPath(configPath);
|
return m_walletController->createWallet(configPath, storagePath, password);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AmmUiBackend::openExisting()
|
bool AmmUiBackend::openExisting()
|
||||||
{
|
{
|
||||||
const QString config = configPath().isEmpty() ? defaultConfigPath() : configPath();
|
return m_walletController->open();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AmmUiBackend::disconnectWallet()
|
void AmmUiBackend::disconnectWallet()
|
||||||
{
|
{
|
||||||
m_wallet->disconnect();
|
m_walletController->disconnect();
|
||||||
setIsWalletOpen(false);
|
|
||||||
m_accountModel->replaceAccounts({});
|
|
||||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(DISCONNECTED_KEY, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString AmmUiBackend::createAccountPublic()
|
void AmmUiBackend::syncWalletState()
|
||||||
{
|
{
|
||||||
const WalletAccountCreation creation = m_wallet->createAccount(true);
|
const WalletUiState& state = m_walletController->state();
|
||||||
if (!creation.ok()) {
|
setIsWalletOpen(state.isWalletOpen);
|
||||||
qWarning() << "AmmUiBackend: public account creation failed"
|
setWalletExists(state.walletExists);
|
||||||
<< walletFailureCode(creation.failure);
|
setConfigPath(state.configPath);
|
||||||
return {};
|
setStoragePath(state.storagePath);
|
||||||
}
|
setWalletHome(state.walletHome);
|
||||||
if (creation.snapshot.ok())
|
setLastSyncedBlock(state.lastSyncedBlock);
|
||||||
applySnapshot(creation.snapshot);
|
setCurrentBlockHeight(state.currentBlockHeight);
|
||||||
else
|
setSequencerAddr(state.sequencerAddress);
|
||||||
qWarning() << "AmmUiBackend: account refresh failed"
|
setSequencerReachable(state.sequencerReachable);
|
||||||
<< 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));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,7 @@
|
|||||||
|
|
||||||
class LogosAPI;
|
class LogosAPI;
|
||||||
class LogosWalletProvider;
|
class LogosWalletProvider;
|
||||||
class QNetworkAccessManager;
|
class WalletController;
|
||||||
class QTimer;
|
|
||||||
|
|
||||||
class AmmUiBackend : public AmmUiBackendSimpleSource {
|
class AmmUiBackend : public AmmUiBackendSimpleSource {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@ -22,7 +21,7 @@ public:
|
|||||||
explicit AmmUiBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
|
explicit AmmUiBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
|
||||||
~AmmUiBackend() override;
|
~AmmUiBackend() override;
|
||||||
|
|
||||||
WalletAccountModel* accountModel() const { return m_accountModel; }
|
WalletAccountModel* accountModel() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
QString createAccountPublic() override;
|
QString createAccountPublic() override;
|
||||||
@ -36,21 +35,11 @@ public slots:
|
|||||||
void disconnectWallet() override;
|
void disconnectWallet() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QString defaultWalletHome();
|
void syncWalletState();
|
||||||
QString defaultConfigPath() const;
|
|
||||||
QString defaultStoragePath() const;
|
|
||||||
|
|
||||||
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;
|
LogosAPI* m_logosAPI;
|
||||||
std::unique_ptr<LogosWalletProvider> m_wallet;
|
std::unique_ptr<LogosWalletProvider> m_wallet;
|
||||||
QNetworkAccessManager* m_net;
|
std::unique_ptr<WalletController> m_walletController;
|
||||||
QTimer* m_reachabilityTimer;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // AMM_UI_BACKEND_H
|
#endif // AMM_UI_BACKEND_H
|
||||||
|
|||||||
@ -18,6 +18,9 @@ if(LOGOS_WALLET_BUILD_ACCESS
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Core)
|
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)
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
|
||||||
if(LOGOS_WALLET_BUILD_ACCESS)
|
if(LOGOS_WALLET_BUILD_ACCESS)
|
||||||
@ -28,6 +31,8 @@ if(LOGOS_WALLET_BUILD_ACCESS)
|
|||||||
src/LogosWalletProvider.cpp
|
src/LogosWalletProvider.cpp
|
||||||
src/WalletAccountModel.h
|
src/WalletAccountModel.h
|
||||||
src/WalletAccountModel.cpp
|
src/WalletAccountModel.cpp
|
||||||
|
src/WalletController.h
|
||||||
|
src/WalletController.cpp
|
||||||
)
|
)
|
||||||
set_target_properties(logos_wallet_access PROPERTIES
|
set_target_properties(logos_wallet_access PROPERTIES
|
||||||
AUTOMOC ON
|
AUTOMOC ON
|
||||||
@ -41,7 +46,10 @@ if(LOGOS_WALLET_BUILD_ACCESS)
|
|||||||
"${LOGOS_WALLET_GENERATED_DIR}"
|
"${LOGOS_WALLET_GENERATED_DIR}"
|
||||||
"${LOGOS_WALLET_GENERATED_DIR}/include"
|
"${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()
|
endif()
|
||||||
|
|
||||||
if(LOGOS_WALLET_BUILD_QML)
|
if(LOGOS_WALLET_BUILD_QML)
|
||||||
@ -127,6 +135,8 @@ if(BUILD_TESTING)
|
|||||||
src/LogosWalletProvider.cpp
|
src/LogosWalletProvider.cpp
|
||||||
src/WalletAccountModel.cpp
|
src/WalletAccountModel.cpp
|
||||||
src/WalletAccountModel.h
|
src/WalletAccountModel.h
|
||||||
|
src/WalletController.cpp
|
||||||
|
src/WalletController.h
|
||||||
)
|
)
|
||||||
set_target_properties(logos_wallet_access_test PROPERTIES AUTOMOC ON)
|
set_target_properties(logos_wallet_access_test PROPERTIES AUTOMOC ON)
|
||||||
target_compile_features(logos_wallet_access_test PRIVATE cxx_std_17)
|
target_compile_features(logos_wallet_access_test PRIVATE cxx_std_17)
|
||||||
@ -135,7 +145,11 @@ if(BUILD_TESTING)
|
|||||||
tests/support
|
tests/support
|
||||||
src
|
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)
|
add_test(NAME logos_wallet_access COMMAND logos_wallet_access_test)
|
||||||
|
|
||||||
if(LOGOS_WALLET_BUILD_QML)
|
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 <QFile>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QSettings>
|
||||||
#include <QSignalSpy>
|
#include <QSignalSpy>
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
|
#include <QTimer>
|
||||||
#include <QtTest>
|
#include <QtTest>
|
||||||
|
|
||||||
#include "FakeWalletProvider.h"
|
#include "FakeWalletProvider.h"
|
||||||
#include "LogosWalletProvider.h"
|
#include "LogosWalletProvider.h"
|
||||||
#include "WalletAccountModel.h"
|
#include "WalletAccountModel.h"
|
||||||
|
#include "WalletController.h"
|
||||||
#include "logos_sdk.h"
|
#include "logos_sdk.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@ -53,6 +57,8 @@ private slots:
|
|||||||
void rejectsInvalidSubmissionResponses();
|
void rejectsInvalidSubmissionResponses();
|
||||||
void exposesStableAccountModelRoles();
|
void exposesStableAccountModelRoles();
|
||||||
void fakeProviderImplementsConsumerContract();
|
void fakeProviderImplementsConsumerContract();
|
||||||
|
void controllerOwnsUiWalletFlow();
|
||||||
|
void controllerStopsReachabilityChecksAfterDisconnect();
|
||||||
};
|
};
|
||||||
|
|
||||||
void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
|
void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
|
||||||
@ -355,6 +361,8 @@ void LogosWalletProviderTest::fakeProviderImplementsConsumerContract()
|
|||||||
|
|
||||||
QCOMPARE(provider.snapshot(true).accounts.size(), 1);
|
QCOMPARE(provider.snapshot(true).accounts.size(), 1);
|
||||||
QVERIFY(provider.lastForceRefresh);
|
QVERIFY(provider.lastForceRefresh);
|
||||||
|
QCOMPARE(provider.readPublicAccount(ACCOUNT_B).accountId, ACCOUNT_B);
|
||||||
|
QCOMPARE(provider.readCalls, 1);
|
||||||
WalletTransaction transaction { PROGRAM_ID, { ACCOUNT_A }, { true }, { 9 } };
|
WalletTransaction transaction { PROGRAM_ID, { ACCOUNT_A }, { true }, { 9 } };
|
||||||
QVERIFY(provider.submitPublicTransaction(transaction).accepted());
|
QVERIFY(provider.submitPublicTransaction(transaction).accepted());
|
||||||
QCOMPARE(provider.lastTransaction.instruction, transaction.instruction);
|
QCOMPARE(provider.lastTransaction.instruction, transaction.instruction);
|
||||||
@ -362,6 +370,83 @@ void LogosWalletProviderTest::fakeProviderImplementsConsumerContract()
|
|||||||
QCOMPARE(provider.disconnectCalls, 1);
|
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)
|
QTEST_GUILESS_MAIN(LogosWalletProviderTest)
|
||||||
|
|
||||||
#include "LogosWalletProviderTest.moc"
|
#include "LogosWalletProviderTest.moc"
|
||||||
|
|||||||
@ -55,10 +55,12 @@ public:
|
|||||||
return createAccountResult;
|
return createAccountResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
WalletAccountRead readPublicAccount(const QString&) const override
|
WalletAccountRead readPublicAccount(const QString& accountId) const override
|
||||||
{
|
{
|
||||||
++readCalls;
|
++readCalls;
|
||||||
return readResult;
|
WalletAccountRead result = readResult;
|
||||||
|
result.accountId = accountId;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
WalletSubmission submitPublicTransaction(
|
WalletSubmission submitPublicTransaction(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user