chore(wallet): synchronize shared wallet module

This commit is contained in:
Ricardo Guilherme Schmidt
2026-07-18 19:38:14 -03:00
parent b71be34edf
commit 9049d31e70
23 changed files with 1535 additions and 257 deletions
@@ -1,13 +1,20 @@
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QHostAddress>
#include <QNetworkAccessManager>
#include <QSettings>
#include <QSignalSpy>
#include <QTcpServer>
#include <QTcpSocket>
#include <QTemporaryDir>
#include <QTimer>
#include <QtTest>
#include <memory>
#include <utility>
#include "FakeWalletProvider.h"
#include "LogosWalletProvider.h"
#include "WalletAccountId.h"
@@ -22,6 +29,30 @@ const QString ACCOUNT_C(64, QLatin1Char('d'));
const QString PROGRAM_ID(64, QLatin1Char('c'));
const QString EOA_OWNER(64, QLatin1Char('0'));
class ScopedEnvironment final {
public:
ScopedEnvironment(QByteArray name, QByteArray value)
: m_name(std::move(name)),
m_hadValue(qEnvironmentVariableIsSet(m_name.constData())),
m_previous(qgetenv(m_name.constData()))
{
qputenv(m_name.constData(), value);
}
~ScopedEnvironment()
{
if (m_hadValue)
qputenv(m_name.constData(), m_previous);
else
qunsetenv(m_name.constData());
}
private:
QByteArray m_name;
bool m_hadValue;
QByteArray m_previous;
};
QString publicAccountJson(const QString& owner = PROGRAM_ID,
const QString& balance = QStringLiteral("01000000000000000000000000000000"),
const QString& nonce = QString(32, QLatin1Char('0')),
@@ -59,16 +90,28 @@ private slots:
void fallsBackToBalanceWhenPublicReadFails();
void createsAndPersistsAccounts();
void preservesCreatedAccountWhenPublicReadFails();
void preservesCreatedAccountWhenSnapshotRefreshFails();
void createdAccountDoesNotRescanWallet();
void dispatchesExactGenericTransaction();
void rejectsInvalidSubmissionResponses();
void encodesAccountIdsForDisplay();
void walletMutationsUseAsyncSdk();
void staleAsyncMutationCannotCrossSession();
void destroyedProviderIgnoresLateMutation();
void exposesStableAccountModelRoles();
void encodesAccountIdsForDisplay();
void persistsHumanizedWalletPreferences();
void fakeProviderImplementsConsumerContract();
void controllerOwnsUiWalletFlow();
void controllerReportsCreationPersistenceAndRefreshFailures();
void controllerSeparatesSnapshotsFromCosmeticState();
void controllerOpenDoesNotWaitForWalletSync();
void controllerCreationDoesNotWaitForWalletSync();
void controllerSeedsDefaultWalletConfigWithConfiguredEndpoint();
void controllerPreservesExistingDefaultWalletConfig();
void controllerStopsReachabilityChecksAfterDisconnect();
void completedAsyncSnapshotReleasesCallback();
void deferredCallbacksIgnoreDestroyedController();
void newerReachabilityResultWins();
void coalescesReachabilityChecksForSameEndpoint();
void controllerReportsPartialWalletCreation();
};
void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
@@ -103,6 +146,7 @@ void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
const int listCalls = modules.logos_execution_zone.listCalls;
const int readCalls = modules.logos_execution_zone.publicReadCalls;
const int saveCalls = modules.logos_execution_zone.saveCalls;
QVERIFY(provider.snapshot().ok());
QCOMPARE(modules.logos_execution_zone.listCalls, listCalls);
QCOMPARE(modules.logos_execution_zone.publicReadCalls, readCalls);
@@ -110,6 +154,7 @@ void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
QVERIFY(provider.snapshot(true).ok());
QVERIFY(modules.logos_execution_zone.listCalls > listCalls);
QVERIFY(modules.logos_execution_zone.publicReadCalls > readCalls);
QCOMPARE(modules.logos_execution_zone.saveCalls, saveCalls);
modules.logos_execution_zone.publicAccounts[ACCOUNT_A] = publicAccountJson(
PROGRAM_ID, QString(32, QLatin1Char('f')));
@@ -236,6 +281,9 @@ void LogosWalletProviderTest::createsAndPersistsWallet()
QVERIFY(directory.isValid());
LogosModules modules;
modules.logos_execution_zone.currentBlockHeight = 12;
modules.logos_execution_zone.accounts = { accountEntry(ACCOUNT_A, true) };
modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson());
LogosWalletProvider provider(&modules);
const WalletPaths paths {
directory.filePath(QStringLiteral("config/wallet.json")),
@@ -249,6 +297,9 @@ void LogosWalletProviderTest::createsAndPersistsWallet()
QCOMPARE(modules.logos_execution_zone.createdStorage, paths.storage);
QCOMPARE(modules.logos_execution_zone.createdPassword, QStringLiteral("secret"));
QVERIFY(modules.logos_execution_zone.saveCalls >= 1);
QCOMPARE(modules.logos_execution_zone.syncCalls, 0);
QCOMPARE(modules.logos_execution_zone.listCalls, 0);
QCOMPARE(modules.logos_execution_zone.publicReadCalls, 0);
LogosModules rejectedModules;
rejectedModules.logos_execution_zone.mnemonic.clear();
@@ -351,7 +402,7 @@ void LogosWalletProviderTest::preservesCreatedAccountWhenPublicReadFails()
QCOMPARE(creation.snapshot.accounts.at(0).balance, QStringLiteral("7"));
}
void LogosWalletProviderTest::preservesCreatedAccountWhenSnapshotRefreshFails()
void LogosWalletProviderTest::createdAccountDoesNotRescanWallet()
{
LogosModules modules;
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
@@ -362,11 +413,13 @@ void LogosWalletProviderTest::preservesCreatedAccountWhenSnapshotRefreshFails()
modules.logos_execution_zone.currentBlockHeight = 1;
modules.logos_execution_zone.syncResult = 1;
const int syncCalls = modules.logos_execution_zone.syncCalls;
const WalletAccountCreation creation = provider.createAccount(true);
QVERIFY(creation.ok());
QCOMPARE(creation.accountId, ACCOUNT_A);
QCOMPARE(creation.snapshot.failure, WalletFailure::ReadFailed);
QVERIFY(creation.snapshot.ok());
QCOMPARE(modules.logos_execution_zone.syncCalls, syncCalls);
}
void LogosWalletProviderTest::dispatchesExactGenericTransaction()
@@ -427,6 +480,108 @@ void LogosWalletProviderTest::rejectsInvalidSubmissionResponses()
WalletFailure::InvalidRequest);
}
void LogosWalletProviderTest::walletMutationsUseAsyncSdk()
{
LogosModules modules;
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
modules.logos_execution_zone.publicAccountId = ACCOUNT_A;
modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson());
modules.logos_execution_zone.transactionResponse = QStringLiteral(
R"({"success":true,"tx_hash":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"})");
LogosWalletProvider provider(&modules);
QVERIFY(provider.connect({}).ok());
modules.logos_execution_zone.deferPublicAccountCreation = true;
bool creationFinished = false;
WalletAccountCreation creation;
const int listCalls = modules.logos_execution_zone.listCalls;
const int syncCalls = modules.logos_execution_zone.syncCalls;
provider.createAccountAsync(true, [&](WalletAccountCreation result) {
creation = std::move(result);
creationFinished = true;
});
QVERIFY(!creationFinished);
QVERIFY(modules.logos_execution_zone.pendingPublicAccountCreation);
modules.logos_execution_zone.finishPublicAccountCreation();
QVERIFY(creationFinished);
QVERIFY(creation.ok());
QVERIFY(creation.publicAccount.ok());
QCOMPARE(creation.accountId, ACCOUNT_A);
QCOMPARE(creation.snapshot.accounts.size(), 1);
QCOMPARE(modules.logos_execution_zone.listCalls, listCalls);
QCOMPARE(modules.logos_execution_zone.syncCalls, syncCalls);
WalletTransaction transaction {
PROGRAM_ID,
{ ACCOUNT_A, ACCOUNT_B },
{ true, false },
{ 7, 0, 4294967295U },
};
modules.logos_execution_zone.deferSubmission = true;
bool submissionFinished = false;
WalletSubmission submission;
provider.submitPublicTransactionAsync(
transaction, [&](WalletSubmission result) {
submission = std::move(result);
submissionFinished = true;
});
QVERIFY(!submissionFinished);
QCOMPARE(modules.logos_execution_zone.submittedProgramId, PROGRAM_ID);
QCOMPARE(modules.logos_execution_zone.submittedAccountIds, transaction.accountIds);
QCOMPARE(modules.logos_execution_zone.submittedSigningRequirements,
QVariantList({ true, false }));
QCOMPARE(modules.logos_execution_zone.submittedInstruction.toList(),
QVariantList({ 7U, 0U, 4294967295U }));
modules.logos_execution_zone.finishSubmission();
QVERIFY(submissionFinished);
QVERIFY(submission.accepted());
QCOMPARE(submission.nativeHash, QString(64, QLatin1Char('a')));
}
void LogosWalletProviderTest::staleAsyncMutationCannotCrossSession()
{
LogosModules modules;
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
modules.logos_execution_zone.publicAccountId = ACCOUNT_A;
modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson());
modules.logos_execution_zone.deferPublicAccountCreation = true;
LogosWalletProvider provider(&modules);
QVERIFY(provider.connect({}).ok());
int callbackCount = 0;
WalletAccountCreation creation;
provider.createAccountAsync(true, [&](WalletAccountCreation result) {
++callbackCount;
creation = std::move(result);
});
provider.disconnect();
modules.logos_execution_zone.finishPublicAccountCreation();
QCOMPARE(callbackCount, 1);
QCOMPARE(creation.failure, WalletFailure::WalletUnavailable);
QCOMPARE(modules.logos_execution_zone.publicReadCalls, 0);
}
void LogosWalletProviderTest::destroyedProviderIgnoresLateMutation()
{
LogosModules modules;
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
modules.logos_execution_zone.transactionResponse = QStringLiteral(
R"({"success":true,"tx_hash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})");
modules.logos_execution_zone.deferSubmission = true;
int callbackCount = 0;
{
LogosWalletProvider provider(&modules);
QVERIFY(provider.connect({}).ok());
provider.submitPublicTransactionAsync(
{ PROGRAM_ID, { ACCOUNT_A }, { true }, { 1 } },
[&](WalletSubmission) { ++callbackCount; });
}
modules.logos_execution_zone.finishSubmission();
QCOMPARE(callbackCount, 0);
}
void LogosWalletProviderTest::exposesStableAccountModelRoles()
{
WalletAccountModel model;
@@ -459,7 +614,7 @@ void LogosWalletProviderTest::exposesStableAccountModelRoles()
QVERIFY(!model.data(model.index(2), WalletAccountModel::CanBePrimaryRole).toBool());
QSignalSpy presentationsChanged(&model, &QAbstractItemModel::dataChanged);
model.applyPresentations({
const QVector<WalletAccountPresentation> presentations {
{
ACCOUNT_A,
QStringLiteral("program"),
@@ -470,7 +625,7 @@ void LogosWalletProviderTest::exposesStableAccountModelRoles()
false,
},
{
ACCOUNT_C,
walletAccountIdToBase58(ACCOUNT_C),
QStringLiteral("token_holding"),
QStringLiteral("TEST holding"),
QStringLiteral("Token"),
@@ -478,7 +633,8 @@ void LogosWalletProviderTest::exposesStableAccountModelRoles()
ACCOUNT_A,
true,
},
});
};
model.applyPresentations(presentations);
QCOMPARE(presentationsChanged.count(), 1);
QCOMPARE(model.data(model.index(2), WalletAccountModel::SectionRole).toString(),
QStringLiteral("hidden"));
@@ -490,6 +646,10 @@ void LogosWalletProviderTest::exposesStableAccountModelRoles()
model.setAlias(ACCOUNT_C, {});
QCOMPARE(model.data(model.index(2), WalletAccountModel::NameRole).toString(),
QStringLiteral("TEST holding"));
QSignalSpy redundantPresentation(&model, &QAbstractItemModel::dataChanged);
QVERIFY(!model.applyPresentations(presentations));
QCOMPARE(redundantPresentation.count(), 0);
}
void LogosWalletProviderTest::encodesAccountIdsForDisplay()
@@ -497,9 +657,16 @@ void LogosWalletProviderTest::encodesAccountIdsForDisplay()
QCOMPARE(walletAccountIdToBase58(
QStringLiteral("00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb")),
QStringLiteral("14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La"));
QCOMPARE(walletAccountIdFromBase58(
QStringLiteral("14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La")),
QStringLiteral("00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb"));
QCOMPARE(walletAccountIdToBase58(QString(64, QLatin1Char('0'))),
QString(32, QLatin1Char('1')));
QCOMPARE(walletAccountIdFromBase58(QString(32, QLatin1Char('1'))),
QString(64, QLatin1Char('0')));
QVERIFY(walletAccountIdToBase58(QStringLiteral("not-an-account-id")).isEmpty());
QVERIFY(walletAccountIdFromBase58(QString(32, QLatin1Char('0'))).isEmpty());
QVERIFY(walletAccountIdFromBase58(QString(45, QLatin1Char('1'))).isEmpty());
}
void LogosWalletProviderTest::persistsHumanizedWalletPreferences()
@@ -599,62 +766,161 @@ void LogosWalletProviderTest::controllerOwnsUiWalletFlow()
settings.clear();
}
void LogosWalletProviderTest::controllerReportsCreationPersistenceAndRefreshFailures()
void LogosWalletProviderTest::controllerSeparatesSnapshotsFromCosmeticState()
{
const QString settingsApplication = QStringLiteral("WalletCreationFailureTest");
const QString settingsApplication = QStringLiteral("WalletSnapshotSignalTest");
QSettings settings(QStringLiteral("Logos"), settingsApplication);
settings.clear();
FakeWalletProvider provider;
provider.connectResult.snapshot.accounts = {
{ ACCOUNT_A, QStringLiteral("5"), true },
};
provider.snapshotResult = provider.connectResult.snapshot;
WalletController controller(provider, settingsApplication);
const WalletUiState baseline = controller.state();
const int baselineAccountCount = controller.accountModel()->count();
QSignalSpy stateChanged(&controller, &WalletController::stateChanged);
QSignalSpy snapshotChanged(&controller, &WalletController::snapshotChanged);
provider.createWalletResult.mnemonic = QStringLiteral("alpha beta gamma");
provider.createWalletResult.failure = WalletFailure::SaveFailed;
provider.createWalletResult.snapshot.failure = WalletFailure::SaveFailed;
QCOMPARE(controller.createWallet(QStringLiteral("config"), QStringLiteral("storage"),
QStringLiteral("secret")),
QString());
QCOMPARE(controller.state().isWalletOpen, baseline.isWalletOpen);
QCOMPARE(controller.state().walletExists, baseline.walletExists);
QCOMPARE(controller.state().syncStatus, baseline.syncStatus);
QCOMPARE(controller.state().syncError, baseline.syncError);
QCOMPARE(controller.accountModel()->count(), baselineAccountCount);
QVERIFY(controller.open());
stateChanged.clear();
snapshotChanged.clear();
QVERIFY(controller.setAccountAlias(ACCOUNT_A, QStringLiteral("Spending")));
QCOMPARE(stateChanged.count(), 1);
QCOMPARE(snapshotChanged.count(), 0);
provider.createWalletResult.failure = WalletFailure::ReadFailed;
provider.createWalletResult.snapshot.failure = WalletFailure::ReadFailed;
QCOMPARE(controller.createWallet(QStringLiteral("config"), QStringLiteral("storage"),
QStringLiteral("secret")),
QStringLiteral("alpha beta gamma"));
QVERIFY(controller.state().isWalletOpen);
QVERIFY(controller.state().walletExists);
QCOMPARE(controller.state().syncStatus, QStringLiteral("error"));
QCOMPARE(controller.state().syncError, QStringLiteral("read_failed"));
QCOMPARE(controller.accountModel()->count(), baselineAccountCount);
QCOMPARE(snapshotChanged.count(), 0);
provider.createAccountResult.accountId = ACCOUNT_B;
provider.createAccountResult.snapshot.failure = WalletFailure::ReadFailed;
QCOMPARE(controller.createAccount(true), ACCOUNT_B);
QCOMPARE(controller.state().syncStatus, QStringLiteral("error"));
QCOMPARE(controller.state().syncError, QStringLiteral("read_failed"));
QCOMPARE(controller.accountModel()->count(), baselineAccountCount);
QCOMPARE(snapshotChanged.count(), 0);
provider.createAccountResult.snapshot = {};
provider.createAccountResult.snapshot.accounts = {
{ ACCOUNT_A, QStringLiteral("5"), true, QStringLiteral("ok"), EOA_OWNER, {} },
{ ACCOUNT_B, QStringLiteral("3"), true, QStringLiteral("ok"), EOA_OWNER, {} },
};
QCOMPARE(controller.createAccount(true), ACCOUNT_B);
QCOMPARE(controller.state().syncStatus, QStringLiteral("ready"));
QVERIFY(controller.state().syncError.isEmpty());
QCOMPARE(controller.accountModel()->count(), 2);
controller.refresh();
QCOMPARE(stateChanged.count(), 3);
QCOMPARE(snapshotChanged.count(), 1);
settings.clear();
}
void LogosWalletProviderTest::controllerOpenDoesNotWaitForWalletSync()
{
const QString settingsApplication = QStringLiteral("WalletAsyncOpenTest");
QSettings settings(QStringLiteral("Logos"), settingsApplication);
settings.clear();
FakeWalletProvider provider;
provider.deferAsync = true;
provider.connectResult.snapshot.accounts = {
{ ACCOUNT_A, QStringLiteral("5"), true },
};
WalletController controller(provider, settingsApplication);
QVERIFY(controller.open());
QCOMPARE(provider.connectCalls, 1);
QVERIFY(!controller.state().isWalletOpen);
QCOMPARE(controller.state().syncStatus, QStringLiteral("opening"));
QCOMPARE(controller.accountModel()->count(), 0);
provider.finishConnect();
QVERIFY(controller.state().isWalletOpen);
QVERIFY(controller.state().canSubmit());
QCOMPARE(controller.state().syncStatus, QStringLiteral("ready"));
QCOMPARE(controller.accountModel()->count(), 1);
settings.clear();
}
void LogosWalletProviderTest::controllerCreationDoesNotWaitForWalletSync()
{
const QString settingsApplication = QStringLiteral("WalletAsyncCreationTest");
QSettings settings(QStringLiteral("Logos"), settingsApplication);
settings.clear();
FakeWalletProvider provider;
provider.deferAsync = true;
provider.createWalletResult.mnemonic = QStringLiteral("one two three");
provider.snapshotResult.accounts = {
{ ACCOUNT_A, QStringLiteral("5"), true },
};
WalletController controller(provider, settingsApplication);
QCOMPARE(controller.createDefaultWallet(QStringLiteral("secret")),
provider.createWalletResult.mnemonic);
QCOMPARE(provider.createWalletCalls, 1);
QCOMPARE(provider.snapshotCalls, 0);
QVERIFY(controller.state().isWalletOpen);
QCOMPARE(controller.state().syncStatus, QStringLiteral("syncing"));
QVERIFY(!controller.state().canSubmit());
QCOMPARE(controller.accountModel()->count(), 0);
QTRY_COMPARE(provider.snapshotCalls, 1);
QVERIFY(provider.lastForceRefresh);
QCOMPARE(controller.state().syncStatus, QStringLiteral("syncing"));
provider.finishSnapshot();
QCOMPARE(controller.state().syncStatus, QStringLiteral("ready"));
QVERIFY(controller.state().canSubmit());
QCOMPARE(controller.accountModel()->count(), 1);
settings.clear();
}
void LogosWalletProviderTest::controllerSeedsDefaultWalletConfigWithConfiguredEndpoint()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
const QString walletHome = directory.filePath(QStringLiteral("wallet"));
ScopedEnvironment walletHomeEnvironment(
QByteArrayLiteral("LEE_WALLET_HOME_DIR"), walletHome.toLocal8Bit());
const QString settingsApplication = QStringLiteral("WalletDefaultEndpointTest");
QSettings settings(QStringLiteral("Logos"), settingsApplication);
settings.clear();
FakeWalletProvider provider;
provider.createWalletResult.mnemonic = QStringLiteral("one two three");
WalletController controller(provider, settingsApplication);
controller.setDefaultSequencerAddress(QStringLiteral("https://testnet.lez.logos.co/"));
QCOMPARE(controller.createDefaultWallet(QStringLiteral("secret")),
provider.createWalletResult.mnemonic);
const QString configPath = walletHome + QStringLiteral("/wallet_config.json");
QCOMPARE(provider.lastPaths.config, configPath);
QFile config(configPath);
QVERIFY(config.open(QIODevice::ReadOnly));
const QJsonDocument document = QJsonDocument::fromJson(config.readAll());
QVERIFY(document.isObject());
const QJsonObject values = document.object();
QCOMPARE(values.value(QStringLiteral("sequencer_addr")).toString(),
QStringLiteral("https://testnet.lez.logos.co/"));
QCOMPARE(values.value(QStringLiteral("seq_poll_timeout")).toString(),
QStringLiteral("12s"));
QCOMPARE(values.value(QStringLiteral("seq_tx_poll_max_blocks")).toInt(), 5);
QCOMPARE(values.value(QStringLiteral("seq_poll_max_retries")).toInt(), 5);
QCOMPARE(values.value(QStringLiteral("seq_block_poll_max_amount")).toInt(), 100);
settings.clear();
}
void LogosWalletProviderTest::controllerPreservesExistingDefaultWalletConfig()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
const QString walletHome = directory.filePath(QStringLiteral("wallet"));
QVERIFY(QDir().mkpath(walletHome));
const QString configPath = walletHome + QStringLiteral("/wallet_config.json");
const QByteArray existingConfig = QByteArrayLiteral(
"{\"sequencer_addr\":\"http://127.0.0.1:3040/\",\"custom\":true}");
QFile config(configPath);
QVERIFY(config.open(QIODevice::WriteOnly));
QCOMPARE(config.write(existingConfig), qint64(existingConfig.size()));
config.close();
ScopedEnvironment walletHomeEnvironment(
QByteArrayLiteral("LEE_WALLET_HOME_DIR"), walletHome.toLocal8Bit());
const QString settingsApplication = QStringLiteral("WalletExistingEndpointTest");
QSettings settings(QStringLiteral("Logos"), settingsApplication);
settings.clear();
FakeWalletProvider provider;
provider.createWalletResult.mnemonic = QStringLiteral("one two three");
WalletController controller(provider, settingsApplication);
controller.setDefaultSequencerAddress(QStringLiteral("https://testnet.lez.logos.co/"));
QCOMPARE(controller.createDefaultWallet(QStringLiteral("secret")),
provider.createWalletResult.mnemonic);
QVERIFY(config.open(QIODevice::ReadOnly));
QCOMPARE(config.readAll(), existingConfig);
settings.clear();
}
@@ -673,19 +939,164 @@ void LogosWalletProviderTest::controllerStopsReachabilityChecksAfterDisconnect()
QVERIFY(controller.open());
QTRY_VERIFY_WITH_TIMEOUT(!finished.isEmpty(), 1000);
controller.disconnect();
finished.clear();
auto* timer = controller.findChild<QTimer*>();
QVERIFY(timer);
QVERIFY(timer->isActive());
controller.disconnect();
QVERIFY(!timer->isActive());
finished.clear();
timer->setInterval(1);
controller.start();
QTest::qWait(50);
QVERIFY(!timer->isActive());
QCOMPARE(finished.count(), 0);
settings.clear();
}
void LogosWalletProviderTest::completedAsyncSnapshotReleasesCallback()
{
LogosModules modules;
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
LogosWalletProvider provider(&modules);
QVERIFY(provider.connect({}).ok());
bool completed = false;
std::weak_ptr<int> callbackLifetime;
{
auto lifetime = std::make_shared<int>(1);
callbackLifetime = lifetime;
provider.snapshotAsync(true,
[lifetime = std::move(lifetime), &completed](WalletSnapshot snapshot) {
QVERIFY(snapshot.ok());
completed = true;
});
}
QVERIFY(completed);
QVERIFY(callbackLifetime.expired());
QCOMPARE(modules.logos_execution_zone.saveCalls, 0);
}
void LogosWalletProviderTest::deferredCallbacksIgnoreDestroyedController()
{
const QString settingsApplication = QStringLiteral("WalletDestroyedCallbackTest");
QSettings settings(QStringLiteral("Logos"), settingsApplication);
settings.clear();
FakeWalletProvider provider;
provider.deferAsync = true;
{
auto controller = std::make_unique<WalletController>(provider, settingsApplication);
QVERIFY(controller->open());
}
provider.finishConnect();
provider.deferAsync = false;
{
auto controller = std::make_unique<WalletController>(provider, settingsApplication);
QVERIFY(controller->open());
provider.deferAsync = true;
controller->refresh();
}
provider.finishSnapshot();
settings.clear();
}
void LogosWalletProviderTest::newerReachabilityResultWins()
{
const QString settingsApplication = QStringLiteral("WalletReachabilityOrderTest");
QSettings settings(QStringLiteral("Logos"), settingsApplication);
settings.clear();
QTcpServer firstServer;
QTcpServer secondServer;
QVERIFY(firstServer.listen(QHostAddress::LocalHost));
QVERIFY(secondServer.listen(QHostAddress::LocalHost));
const QString firstEndpoint = QStringLiteral("http://127.0.0.1:%1")
.arg(firstServer.serverPort());
const QString secondEndpoint = QStringLiteral("http://127.0.0.1:%1")
.arg(secondServer.serverPort());
FakeWalletProvider provider;
provider.connectResult.snapshot.sequencerAddress = firstEndpoint;
WalletController controller(provider, settingsApplication);
auto* network = controller.findChild<QNetworkAccessManager*>();
QVERIFY(network);
QSignalSpy finished(network, &QNetworkAccessManager::finished);
QVERIFY(controller.open());
QTRY_VERIFY(firstServer.hasPendingConnections());
QTcpSocket* first = firstServer.nextPendingConnection();
QVERIFY(first);
provider.createAccountResult.accountId = ACCOUNT_B;
provider.createAccountResult.snapshot.sequencerAddress = secondEndpoint;
QCOMPARE(controller.createAccount(true), ACCOUNT_B);
QTRY_VERIFY(secondServer.hasPendingConnections());
QTcpSocket* second = secondServer.nextPendingConnection();
QVERIFY(second);
second->write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
second->disconnectFromHost();
QTRY_COMPARE(finished.size(), 1);
QVERIFY(controller.state().sequencerReachable);
first->disconnectFromHost();
QTRY_COMPARE(finished.size(), 2);
QVERIFY(controller.state().sequencerReachable);
settings.clear();
}
void LogosWalletProviderTest::coalescesReachabilityChecksForSameEndpoint()
{
const QString settingsApplication = QStringLiteral("WalletReachabilityCoalesceTest");
QSettings settings(QStringLiteral("Logos"), settingsApplication);
settings.clear();
QTcpServer server;
QVERIFY(server.listen(QHostAddress::LocalHost));
const QString endpoint = QStringLiteral("http://127.0.0.1:%1").arg(server.serverPort());
FakeWalletProvider provider;
provider.connectResult.snapshot.sequencerAddress = endpoint;
WalletController controller(provider, settingsApplication);
QVERIFY(controller.open());
QTRY_VERIFY(server.hasPendingConnections());
QTcpSocket* request = server.nextPendingConnection();
QVERIFY(request);
provider.createAccountResult.accountId = ACCOUNT_B;
provider.createAccountResult.snapshot.sequencerAddress = endpoint;
QCOMPARE(controller.createAccount(true), ACCOUNT_B);
QTest::qWait(50);
QVERIFY(!server.hasPendingConnections());
request->write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
request->disconnectFromHost();
settings.clear();
}
void LogosWalletProviderTest::controllerReportsPartialWalletCreation()
{
const QString settingsApplication = QStringLiteral("WalletPartialCreationTest");
QSettings settings(QStringLiteral("Logos"), settingsApplication);
settings.clear();
FakeWalletProvider provider;
provider.createWalletResult.mnemonic = QStringLiteral("one two three");
provider.createWalletResult.failure = WalletFailure::SaveFailed;
WalletController controller(provider, settingsApplication);
QCOMPARE(controller.createDefaultWallet(QStringLiteral("secret")),
provider.createWalletResult.mnemonic);
QVERIFY(!controller.state().isWalletOpen);
QCOMPARE(controller.state().syncStatus, QStringLiteral("error"));
QCOMPARE(controller.state().syncError, QStringLiteral("save_failed"));
settings.clear();
}
QTEST_GUILESS_MAIN(LogosWalletProviderTest)
#include "LogosWalletProviderTest.moc"
@@ -7,6 +7,7 @@
#include <QVariantList>
#include <functional>
#include <utility>
class LogosAPI;
@@ -32,6 +33,10 @@ public:
int listCalls = 0;
int publicReadCalls = 0;
int submitCalls = 0;
bool deferPublicAccountCreation = false;
bool deferSubmission = false;
std::function<void(QString)> pendingPublicAccountCreation;
std::function<void(QString)> pendingSubmission;
QString openedConfig;
QString openedStorage;
QString createdConfig;
@@ -77,6 +82,17 @@ public:
QString create_account_public() { return publicAccountId; }
QString create_account_private() { return privateAccountId; }
void create_account_publicAsync(std::function<void(QString)> callback)
{
if (deferPublicAccountCreation)
pendingPublicAccountCreation = std::move(callback);
else
callback(create_account_public());
}
void create_account_privateAsync(std::function<void(QString)> callback)
{
callback(create_account_private());
}
int get_last_synced_block() const { return lastSyncedBlock; }
int get_current_block_height() const { return currentBlockHeight; }
@@ -150,6 +166,38 @@ public:
submittedProgramId = programId;
return transactionResponse;
}
void send_generic_public_transactionAsync(
const QStringList& accountIds,
const QVariantList& signingRequirements,
const QVariant& instruction,
const QString& programId,
std::function<void(QString)> callback)
{
++submitCalls;
submittedAccountIds = accountIds;
submittedSigningRequirements = signingRequirements;
submittedInstruction = instruction;
submittedProgramId = programId;
if (deferSubmission)
pendingSubmission = std::move(callback);
else
callback(transactionResponse);
}
void finishPublicAccountCreation()
{
auto callback = std::move(pendingPublicAccountCreation);
if (callback)
callback(publicAccountId);
}
void finishSubmission()
{
auto callback = std::move(pendingSubmission);
if (callback)
callback(transactionResponse);
}
};
struct LogosModules {