mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
feat(wallet): humanize shared wallet experience
Consolidate wallet account decoding and portfolio handling around the shared Rust IDL decoder. Simplify AMM wallet integration, remove obsolete caches and network plumbing, and cover account selection and live flows.
This commit is contained in:
@@ -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 "WalletAccountModel.h"
|
||||
@@ -17,7 +24,33 @@
|
||||
namespace {
|
||||
const QString ACCOUNT_A(64, QLatin1Char('a'));
|
||||
const QString ACCOUNT_B(64, QLatin1Char('b'));
|
||||
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"),
|
||||
@@ -39,6 +72,7 @@ QVariantMap accountEntry(const QString& id, bool isPublic)
|
||||
{ QStringLiteral("is_public"), isPublic },
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LogosWalletProviderTest : public QObject {
|
||||
@@ -47,18 +81,33 @@ class LogosWalletProviderTest : public QObject {
|
||||
private slots:
|
||||
void adoptsOpenWalletAndCachesSnapshots();
|
||||
void opensConfiguredWalletWhenNoSharedSessionExists();
|
||||
void opensStoredWalletAsynchronouslyWithoutAccountProbe();
|
||||
void opensAndReadsAsynchronously();
|
||||
void avoidsSavingAfterUnchangedAsynchronousSnapshots();
|
||||
void createsAndPersistsWallet();
|
||||
void validatesCompletePublicAccountPayloads();
|
||||
void fallsBackToBalanceWhenPublicReadFails();
|
||||
void createsAndPersistsAccounts();
|
||||
void preservesCreatedAccountWhenPublicReadFails();
|
||||
void preservesCreatedAccountWhenSnapshotRefreshFails();
|
||||
void createdAccountDoesNotRescanWallet();
|
||||
void dispatchesExactGenericTransaction();
|
||||
void rejectsInvalidSubmissionResponses();
|
||||
void exposesStableAccountModelRoles();
|
||||
void clearsStaleAccountPresentationsWithoutInvalidatingPrimary();
|
||||
void persistsHumanizedWalletPreferences();
|
||||
void fakeProviderImplementsConsumerContract();
|
||||
void controllerOwnsUiWalletFlow();
|
||||
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()
|
||||
@@ -84,11 +133,18 @@ void LogosWalletProviderTest::adoptsOpenWalletAndCachesSnapshots()
|
||||
QCOMPARE(session.snapshot.accounts.at(0).balance, QStringLiteral("1"));
|
||||
QCOMPARE(session.snapshot.accounts.at(1).balance, QStringLiteral("42"));
|
||||
QCOMPARE(session.snapshot.publicAccountReads.size(), 1);
|
||||
QCOMPARE(session.snapshot.accounts.at(0).readStatus, QStringLiteral("ok"));
|
||||
QCOMPARE(session.snapshot.accounts.at(0).programOwner, PROGRAM_ID);
|
||||
QCOMPARE(session.snapshot.accounts.at(0).dataHex, QStringLiteral("00ff"));
|
||||
QCOMPARE(session.snapshot.accounts.at(0).displayAddress,
|
||||
QStringLiteral("base58-") + ACCOUNT_A);
|
||||
QCOMPARE(session.snapshot.accounts.at(1).readStatus, QStringLiteral("private"));
|
||||
QCOMPARE(session.snapshot.currentBlockHeight, quint64(12));
|
||||
QCOMPARE(session.snapshot.lastSyncedBlock, quint64(11));
|
||||
|
||||
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);
|
||||
@@ -96,6 +152,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')));
|
||||
@@ -131,6 +188,7 @@ void LogosWalletProviderTest::opensConfiguredWalletWhenNoSharedSessionExists()
|
||||
QVERIFY(!session.adopted);
|
||||
QCOMPARE(modules.logos_execution_zone.openCalls, 1);
|
||||
QCOMPARE(modules.logos_execution_zone.openedStorage, storage);
|
||||
QCOMPARE(modules.logos_execution_zone.listCalls, 1);
|
||||
|
||||
LogosModules missingModules;
|
||||
LogosWalletProvider missingProvider(&missingModules);
|
||||
@@ -138,12 +196,83 @@ void LogosWalletProviderTest::opensConfiguredWalletWhenNoSharedSessionExists()
|
||||
WalletFailure::WalletMissing);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::opensStoredWalletAsynchronouslyWithoutAccountProbe()
|
||||
{
|
||||
QTemporaryDir directory;
|
||||
QVERIFY(directory.isValid());
|
||||
const QString storage = directory.filePath(QStringLiteral("storage.json"));
|
||||
QFile file(storage);
|
||||
QVERIFY(file.open(QIODevice::WriteOnly));
|
||||
file.close();
|
||||
|
||||
LogosModules modules;
|
||||
LogosWalletProvider provider(&modules);
|
||||
bool connected = false;
|
||||
provider.connectAsync({ directory.filePath(QStringLiteral("wallet.json")), storage },
|
||||
[&connected](WalletSession session) {
|
||||
connected = session.ok() && !session.adopted;
|
||||
});
|
||||
|
||||
QVERIFY(connected);
|
||||
QCOMPARE(modules.logos_execution_zone.openCalls, 1);
|
||||
QCOMPARE(modules.logos_execution_zone.listCalls, 1);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::opensAndReadsAsynchronously()
|
||||
{
|
||||
LogosModules modules;
|
||||
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
|
||||
modules.logos_execution_zone.accounts = { accountEntry(ACCOUNT_A, true) };
|
||||
modules.logos_execution_zone.publicAccounts.insert(
|
||||
ACCOUNT_A, publicAccountJson(EOA_OWNER));
|
||||
LogosWalletProvider provider(&modules);
|
||||
|
||||
bool connected = false;
|
||||
provider.connectAsync({}, [&connected](WalletSession session) {
|
||||
connected = session.ok() && session.snapshot.accounts.size() == 1;
|
||||
});
|
||||
QVERIFY(connected);
|
||||
|
||||
bool refreshed = false;
|
||||
provider.snapshotAsync(true, [&refreshed](WalletSnapshot snapshot) {
|
||||
refreshed = snapshot.ok() && snapshot.accounts.at(0).programOwner == EOA_OWNER;
|
||||
});
|
||||
QVERIFY(refreshed);
|
||||
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::avoidsSavingAfterUnchangedAsynchronousSnapshots()
|
||||
{
|
||||
LogosModules modules;
|
||||
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
|
||||
modules.logos_execution_zone.currentBlockHeight = 12;
|
||||
modules.logos_execution_zone.lastSyncedBlock = 12;
|
||||
LogosWalletProvider provider(&modules);
|
||||
|
||||
bool connected = false;
|
||||
provider.connectAsync({}, [&connected](WalletSession session) {
|
||||
connected = session.ok();
|
||||
});
|
||||
QVERIFY(connected);
|
||||
QCOMPARE(modules.logos_execution_zone.saveCalls, 0);
|
||||
|
||||
bool refreshed = false;
|
||||
provider.snapshotAsync(true, [&refreshed](WalletSnapshot snapshot) {
|
||||
refreshed = snapshot.ok();
|
||||
});
|
||||
QVERIFY(refreshed);
|
||||
QCOMPARE(modules.logos_execution_zone.saveCalls, 0);
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::createsAndPersistsWallet()
|
||||
{
|
||||
QTemporaryDir directory;
|
||||
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")),
|
||||
@@ -157,6 +286,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();
|
||||
@@ -227,12 +359,14 @@ void LogosWalletProviderTest::createsAndPersistsAccounts()
|
||||
QVERIFY(provider.connect({}).ok());
|
||||
|
||||
const int savesBeforeCreate = modules.logos_execution_zone.saveCalls;
|
||||
const int publicReadsBeforeCreate = modules.logos_execution_zone.publicReadCalls;
|
||||
const WalletAccountCreation creation = provider.createAccount(true);
|
||||
QVERIFY(creation.ok());
|
||||
QCOMPARE(creation.accountId, ACCOUNT_A);
|
||||
QVERIFY(creation.publicAccount.ok());
|
||||
QCOMPARE(creation.snapshot.accounts.size(), 1);
|
||||
QVERIFY(modules.logos_execution_zone.saveCalls > savesBeforeCreate);
|
||||
QCOMPARE(modules.logos_execution_zone.publicReadCalls, publicReadsBeforeCreate + 1);
|
||||
|
||||
modules.logos_execution_zone.saveResult = 1;
|
||||
QCOMPARE(provider.createAccount(true).failure, WalletFailure::SaveFailed);
|
||||
@@ -257,7 +391,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");
|
||||
@@ -268,11 +402,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()
|
||||
@@ -298,8 +434,8 @@ void LogosWalletProviderTest::dispatchesExactGenericTransaction()
|
||||
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 }));
|
||||
QCOMPARE(modules.logos_execution_zone.submittedInstruction.toByteArray(),
|
||||
QByteArray::fromHex("0700000000000000ffffffff"));
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::rejectsInvalidSubmissionResponses()
|
||||
@@ -338,19 +474,171 @@ void LogosWalletProviderTest::exposesStableAccountModelRoles()
|
||||
WalletAccountModel model;
|
||||
QSignalSpy countChanged(&model, &WalletAccountModel::countChanged);
|
||||
model.replaceAccounts({
|
||||
{ ACCOUNT_A, QStringLiteral("10"), true },
|
||||
{ ACCOUNT_B, QStringLiteral("20"), false },
|
||||
});
|
||||
{ ACCOUNT_A, QStringLiteral("10"), true, QStringLiteral("ok"), EOA_OWNER, {} },
|
||||
{ ACCOUNT_B, QStringLiteral("20"), false, QStringLiteral("private"), {}, {} },
|
||||
{ ACCOUNT_C, QStringLiteral("30"), true, QStringLiteral("ok"), PROGRAM_ID, QStringLiteral("00") },
|
||||
}, { { ACCOUNT_A, QStringLiteral("Trading") } }, ACCOUNT_A);
|
||||
|
||||
QCOMPARE(model.count(), 2);
|
||||
QCOMPARE(model.count(), 3);
|
||||
QCOMPARE(countChanged.count(), 1);
|
||||
QCOMPARE(model.roleNames().value(WalletAccountModel::NameRole), QByteArray("name"));
|
||||
QCOMPARE(model.data(model.index(0), WalletAccountModel::NameRole).toString(),
|
||||
QStringLiteral("Account 1"));
|
||||
QStringLiteral("Trading"));
|
||||
QCOMPARE(model.data(model.index(0), WalletAccountModel::KindRole).toString(),
|
||||
QStringLiteral("user"));
|
||||
QVERIFY(model.data(model.index(0), WalletAccountModel::CanBePrimaryRole).toBool());
|
||||
QVERIFY(model.data(model.index(0), WalletAccountModel::IsPrimaryRole).toBool());
|
||||
QCOMPARE(model.data(model.index(1), WalletAccountModel::AddressRole).toString(), ACCOUNT_B);
|
||||
QCOMPARE(model.roleNames().value(WalletAccountModel::DisplayAddressRole),
|
||||
QByteArray("displayAddress"));
|
||||
QCOMPARE(model.roleNames().value(WalletAccountModel::DecodedDataRole),
|
||||
QByteArray("decodedData"));
|
||||
QCOMPARE(model.data(model.index(1), WalletAccountModel::DisplayAddressRole).toString(),
|
||||
ACCOUNT_B);
|
||||
QCOMPARE(model.data(model.index(1), WalletAccountModel::BalanceRole).toString(),
|
||||
QStringLiteral("20"));
|
||||
QVERIFY(!model.data(model.index(1), WalletAccountModel::IsPublicRole).toBool());
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::KindRole).toString(),
|
||||
QStringLiteral("program"));
|
||||
QVERIFY(!model.data(model.index(2), WalletAccountModel::CanBePrimaryRole).toBool());
|
||||
|
||||
QSignalSpy presentationsChanged(&model, &QAbstractItemModel::dataChanged);
|
||||
const QVector<WalletAccountPresentation> presentations {
|
||||
{
|
||||
ACCOUNT_A,
|
||||
QStringLiteral("program"),
|
||||
{},
|
||||
QStringLiteral("System"),
|
||||
QStringLiteral("UserAccount"),
|
||||
{},
|
||||
false,
|
||||
QStringLiteral("{\"public_key\":\"Public/test\"}"),
|
||||
},
|
||||
{
|
||||
ACCOUNT_C,
|
||||
QStringLiteral("token_holding"),
|
||||
QStringLiteral("TEST holding"),
|
||||
QStringLiteral("Token"),
|
||||
QStringLiteral("TokenHolding"),
|
||||
ACCOUNT_A,
|
||||
true,
|
||||
},
|
||||
};
|
||||
model.applyPresentations(presentations);
|
||||
QCOMPARE(presentationsChanged.count(), 1);
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::SectionRole).toString(),
|
||||
QStringLiteral("hidden"));
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::NameRole).toString(),
|
||||
QStringLiteral("TEST holding"));
|
||||
QCOMPARE(model.data(model.index(0), WalletAccountModel::DecodedDataRole).toString(),
|
||||
QStringLiteral("{\"public_key\":\"Public/test\"}"));
|
||||
model.setAlias(ACCOUNT_C, QStringLiteral("Reserve"));
|
||||
QCOMPARE(model.data(model.index(2), WalletAccountModel::NameRole).toString(),
|
||||
QStringLiteral("Reserve"));
|
||||
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::clearsStaleAccountPresentationsWithoutInvalidatingPrimary()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletPresentationClearingTest");
|
||||
QSettings settings(QStringLiteral("Logos"), settingsApplication);
|
||||
settings.clear();
|
||||
|
||||
FakeWalletProvider provider;
|
||||
provider.connectResult.adopted = true;
|
||||
provider.connectResult.snapshot.accounts = {
|
||||
{ ACCOUNT_A, QStringLiteral("10"), true, QStringLiteral("ok"), EOA_OWNER, {} },
|
||||
{ ACCOUNT_C, QStringLiteral("20"), true, QStringLiteral("ok"), PROGRAM_ID,
|
||||
QStringLiteral("00ff") },
|
||||
};
|
||||
WalletController controller(provider, settingsApplication);
|
||||
|
||||
QVERIFY(controller.open());
|
||||
QCOMPARE(controller.state().primaryAccountAddress, ACCOUNT_A);
|
||||
QCOMPARE(controller.state().primaryAccountName, QStringLiteral("User account"));
|
||||
|
||||
controller.applyAccountPresentations({
|
||||
{
|
||||
ACCOUNT_C,
|
||||
QStringLiteral("token_holding"),
|
||||
QStringLiteral("TEST holding"),
|
||||
QStringLiteral("Token"),
|
||||
QStringLiteral("TokenHolding"),
|
||||
ACCOUNT_A,
|
||||
true,
|
||||
QStringLiteral("{\"amount\":\"20\"}"),
|
||||
},
|
||||
});
|
||||
|
||||
WalletAccountModel* model = controller.accountModel();
|
||||
const QModelIndex holding = model->index(model->indexOf(ACCOUNT_C));
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::KindRole).toString(),
|
||||
QStringLiteral("token_holding"));
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::SectionRole).toString(),
|
||||
QStringLiteral("hidden"));
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::NameRole).toString(),
|
||||
QStringLiteral("TEST holding"));
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::DecodedDataRole).toString(),
|
||||
QStringLiteral("{\"amount\":\"20\"}"));
|
||||
|
||||
controller.applyAccountPresentations({});
|
||||
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::KindRole).toString(),
|
||||
QStringLiteral("program"));
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::SectionRole).toString(),
|
||||
QStringLiteral("advanced"));
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::NameRole).toString(),
|
||||
QStringLiteral("Program account"));
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::ProgramNameRole).toString(), QString());
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::AccountTypeRole).toString(), QString());
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::DefinitionIdRole).toString(), QString());
|
||||
QCOMPARE(model->data(holding, WalletAccountModel::DecodedDataRole).toString(), QString());
|
||||
QVERIFY(!model->data(holding, WalletAccountModel::CanBePrimaryRole).toBool());
|
||||
|
||||
const QModelIndex primary = model->index(model->indexOf(ACCOUNT_A));
|
||||
QVERIFY(model->data(primary, WalletAccountModel::CanBePrimaryRole).toBool());
|
||||
QVERIFY(model->data(primary, WalletAccountModel::IsPrimaryRole).toBool());
|
||||
QCOMPARE(controller.state().primaryAccountAddress, ACCOUNT_A);
|
||||
QCOMPARE(controller.state().primaryAccountName, QStringLiteral("User account"));
|
||||
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::persistsHumanizedWalletPreferences()
|
||||
{
|
||||
const QString application = QStringLiteral("HumanizedWalletPreferencesTest");
|
||||
QSettings settings(QStringLiteral("Logos"), application);
|
||||
settings.clear();
|
||||
FakeWalletProvider provider;
|
||||
provider.connectResult.adopted = true;
|
||||
provider.connectResult.snapshot.accounts = {
|
||||
{ ACCOUNT_A, QStringLiteral("10"), true, QStringLiteral("ok"), EOA_OWNER, {} },
|
||||
{ ACCOUNT_B, QStringLiteral("20"), false, QStringLiteral("private"), {}, {} },
|
||||
{ ACCOUNT_C, QStringLiteral("30"), true, QStringLiteral("ok"), PROGRAM_ID, {} },
|
||||
};
|
||||
|
||||
{
|
||||
WalletController controller(provider, application);
|
||||
QVERIFY(controller.open());
|
||||
QCOMPARE(controller.state().primaryAccountAddress, ACCOUNT_A);
|
||||
QVERIFY(!controller.setPrimaryAccount(ACCOUNT_C));
|
||||
QVERIFY(controller.setAccountAlias(ACCOUNT_B, QStringLiteral(" Private savings ")));
|
||||
QVERIFY(controller.setPrimaryAccount(ACCOUNT_B));
|
||||
QCOMPARE(controller.state().primaryAccountName, QStringLiteral("Private savings"));
|
||||
QVERIFY(!controller.setAccountAlias(ACCOUNT_A, QString(41, QLatin1Char('x'))));
|
||||
}
|
||||
|
||||
WalletController reopened(provider, application);
|
||||
QVERIFY(reopened.open());
|
||||
QCOMPARE(reopened.state().primaryAccountAddress, ACCOUNT_B);
|
||||
QCOMPARE(reopened.state().primaryAccountName, QStringLiteral("Private savings"));
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::fakeProviderImplementsConsumerContract()
|
||||
@@ -419,6 +707,164 @@ void LogosWalletProviderTest::controllerOwnsUiWalletFlow()
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerSeparatesSnapshotsFromCosmeticState()
|
||||
{
|
||||
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);
|
||||
QSignalSpy stateChanged(&controller, &WalletController::stateChanged);
|
||||
QSignalSpy snapshotChanged(&controller, &WalletController::snapshotChanged);
|
||||
|
||||
QVERIFY(controller.open());
|
||||
stateChanged.clear();
|
||||
snapshotChanged.clear();
|
||||
|
||||
QVERIFY(controller.setAccountAlias(ACCOUNT_A, QStringLiteral("Spending")));
|
||||
QCOMPARE(stateChanged.count(), 1);
|
||||
QCOMPARE(snapshotChanged.count(), 0);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
void LogosWalletProviderTest::controllerStopsReachabilityChecksAfterDisconnect()
|
||||
{
|
||||
const QString settingsApplication = QStringLiteral("WalletReachabilityTest");
|
||||
@@ -434,19 +880,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"
|
||||
|
||||
Reference in New Issue
Block a user