mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
perf(amm): cache verified token definitions
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
#include "AmmUiBackend.h"
|
||||
#include "FakeWalletProvider.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QHash>
|
||||
#include <QHostAddress>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSettings>
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QTemporaryDir>
|
||||
#include <QtTest>
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
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;
|
||||
};
|
||||
|
||||
class LocalRpcServer final {
|
||||
public:
|
||||
explicit LocalRpcServer(QString channelId)
|
||||
: m_channelId(std::move(channelId))
|
||||
{
|
||||
QObject::connect(&m_server, &QTcpServer::newConnection, [&]() {
|
||||
while (m_server.hasPendingConnections()) {
|
||||
QTcpSocket* socket = m_server.nextPendingConnection();
|
||||
QObject::connect(socket, &QTcpSocket::readyRead, socket,
|
||||
[this, socket]() { process(socket); });
|
||||
if (socket->bytesAvailable() > 0)
|
||||
process(socket);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool listen()
|
||||
{
|
||||
return m_server.listen(QHostAddress::LocalHost);
|
||||
}
|
||||
|
||||
QString endpoint() const
|
||||
{
|
||||
return QStringLiteral("http://127.0.0.1:%1").arg(m_server.serverPort());
|
||||
}
|
||||
|
||||
private:
|
||||
void process(QTcpSocket* socket)
|
||||
{
|
||||
QByteArray& request = m_requests[socket];
|
||||
request.append(socket->readAll());
|
||||
const qsizetype headerEnd = request.indexOf("\r\n\r\n");
|
||||
if (headerEnd < 0)
|
||||
return;
|
||||
|
||||
qsizetype contentLength = 0;
|
||||
for (QByteArray line : request.first(headerEnd).split('\n')) {
|
||||
line = line.trimmed();
|
||||
if (line.toLower().startsWith("content-length:")) {
|
||||
contentLength = line.mid(sizeof("content-length:") - 1)
|
||||
.trimmed().toLongLong();
|
||||
}
|
||||
}
|
||||
if (request.size() - headerEnd - 4 < contentLength)
|
||||
return;
|
||||
|
||||
m_requests.remove(socket);
|
||||
const QByteArray payload = QJsonDocument(QJsonObject {
|
||||
{ QStringLiteral("jsonrpc"), QStringLiteral("2.0") },
|
||||
{ QStringLiteral("id"), 1 },
|
||||
{ QStringLiteral("result"), m_channelId },
|
||||
}).toJson(QJsonDocument::Compact);
|
||||
QByteArray response = QByteArrayLiteral(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ");
|
||||
response += QByteArray::number(payload.size());
|
||||
response += QByteArrayLiteral("\r\nConnection: close\r\n\r\n");
|
||||
response += payload;
|
||||
socket->write(response);
|
||||
socket->disconnectFromHost();
|
||||
}
|
||||
|
||||
QTcpServer m_server;
|
||||
QHash<QTcpSocket*, QByteArray> m_requests;
|
||||
QString m_channelId;
|
||||
};
|
||||
|
||||
QByteArray devnetConfig(const QString& channelId,
|
||||
const QString& ammProgramId,
|
||||
const QString& definitionId)
|
||||
{
|
||||
return QJsonDocument(QJsonObject {
|
||||
{ QStringLiteral("channelId"), channelId },
|
||||
{ QStringLiteral("ammProgramId"), ammProgramId },
|
||||
{ QStringLiteral("tokenDefinitionIds"), QJsonArray { definitionId } },
|
||||
}).toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
class BackendFixture final {
|
||||
public:
|
||||
BackendFixture()
|
||||
: channelId(64, QLatin1Char('a')),
|
||||
ammProgramId(64, QLatin1Char('b')),
|
||||
definitionId(64, QLatin1Char('c')),
|
||||
tokenProgramId(64, QLatin1Char('d')),
|
||||
server(channelId)
|
||||
{
|
||||
}
|
||||
|
||||
bool initialize(bool deferDefinitionReads = false)
|
||||
{
|
||||
if (!server.listen() || !directory.isValid())
|
||||
return false;
|
||||
const QString walletHome = directory.filePath(QStringLiteral("wallet"));
|
||||
if (!QDir().mkpath(walletHome))
|
||||
return false;
|
||||
const QString configPath = directory.filePath(QStringLiteral("devnet.json"));
|
||||
QFile config(configPath);
|
||||
if (!config.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
const QByteArray configData = devnetConfig(channelId, ammProgramId, definitionId);
|
||||
if (config.write(configData) != qint64(configData.size()))
|
||||
return false;
|
||||
config.close();
|
||||
|
||||
network = std::make_unique<ScopedEnvironment>(
|
||||
QByteArrayLiteral("AMM_UI_NETWORK"), QByteArrayLiteral("devnet"));
|
||||
devnetFile = std::make_unique<ScopedEnvironment>(
|
||||
QByteArrayLiteral("AMM_UI_DEVNET_FILE"), configPath.toLocal8Bit());
|
||||
walletHomeEnvironment = std::make_unique<ScopedEnvironment>(
|
||||
QByteArrayLiteral("LEE_WALLET_HOME_DIR"), walletHome.toLocal8Bit());
|
||||
settingsHome = std::make_unique<ScopedEnvironment>(
|
||||
QByteArrayLiteral("XDG_CONFIG_HOME"),
|
||||
directory.filePath(QStringLiteral("settings")).toLocal8Bit());
|
||||
QSettings settings(QStringLiteral("Logos"), QStringLiteral("AmmUI"));
|
||||
settings.setValue(QStringLiteral("disconnected"), false);
|
||||
settings.sync();
|
||||
|
||||
provider.connectResult.adopted = true;
|
||||
provider.connectResult.snapshot.sequencerAddress = server.endpoint();
|
||||
provider.snapshotResult = provider.connectResult.snapshot;
|
||||
provider.readResult.status = QStringLiteral("ok");
|
||||
provider.readResult.programOwner = tokenProgramId;
|
||||
provider.readResult.dataHex = QStringLiteral(
|
||||
"0004000000544553540a0000000000000000000000000000000000");
|
||||
provider.deferPublicAccountReads = deferDefinitionReads;
|
||||
backend = std::make_unique<AmmUiBackend>(provider);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString channelId;
|
||||
QString ammProgramId;
|
||||
QString definitionId;
|
||||
QString tokenProgramId;
|
||||
LocalRpcServer server;
|
||||
QTemporaryDir directory;
|
||||
std::unique_ptr<ScopedEnvironment> network;
|
||||
std::unique_ptr<ScopedEnvironment> devnetFile;
|
||||
std::unique_ptr<ScopedEnvironment> walletHomeEnvironment;
|
||||
std::unique_ptr<ScopedEnvironment> settingsHome;
|
||||
FakeWalletProvider provider;
|
||||
std::unique_ptr<AmmUiBackend> backend;
|
||||
};
|
||||
}
|
||||
|
||||
class AmmUiBackendDefinitionCacheTest : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void reusesDefinitionsAfterRefreshAndReopen();
|
||||
void restartsDefinitionReadAfterRefreshAndReopen();
|
||||
};
|
||||
|
||||
void AmmUiBackendDefinitionCacheTest::reusesDefinitionsAfterRefreshAndReopen()
|
||||
{
|
||||
BackendFixture fixture;
|
||||
QVERIFY(fixture.initialize());
|
||||
|
||||
QTRY_COMPARE(fixture.backend->networkStatus(), QStringLiteral("ready"));
|
||||
QTRY_COMPARE(fixture.backend->assetStatus(), QStringLiteral("ready"));
|
||||
QCOMPARE(fixture.provider.publicAccountReadCalls, 1);
|
||||
QCOMPARE(fixture.provider.lastPublicAccountIds,
|
||||
QStringList { fixture.definitionId });
|
||||
|
||||
fixture.backend->refreshBalances();
|
||||
QTRY_COMPARE(fixture.backend->assetStatus(), QStringLiteral("ready"));
|
||||
QCOMPARE(fixture.provider.publicAccountReadCalls, 1);
|
||||
|
||||
fixture.backend->disconnectWallet();
|
||||
QTRY_COMPARE(fixture.backend->walletSyncStatus(), QStringLiteral("closed"));
|
||||
QVERIFY(fixture.backend->openExisting());
|
||||
QTRY_COMPARE(fixture.backend->assetStatus(), QStringLiteral("ready"));
|
||||
QCOMPARE(fixture.provider.publicAccountReadCalls, 1);
|
||||
}
|
||||
|
||||
void AmmUiBackendDefinitionCacheTest::restartsDefinitionReadAfterRefreshAndReopen()
|
||||
{
|
||||
BackendFixture fixture;
|
||||
QVERIFY(fixture.initialize(true));
|
||||
|
||||
QTRY_COMPARE(fixture.backend->networkStatus(), QStringLiteral("ready"));
|
||||
QTRY_COMPARE(fixture.provider.publicAccountReadCalls, 1);
|
||||
QCOMPARE(fixture.backend->assetStatus(), QStringLiteral("loading"));
|
||||
|
||||
fixture.backend->refreshBalances();
|
||||
QTRY_COMPARE(fixture.provider.publicAccountReadCalls, 2);
|
||||
|
||||
fixture.backend->disconnectWallet();
|
||||
QTRY_COMPARE(fixture.backend->walletSyncStatus(), QStringLiteral("closed"));
|
||||
QVERIFY(fixture.backend->openExisting());
|
||||
QTRY_COMPARE(fixture.provider.publicAccountReadCalls, 3);
|
||||
|
||||
fixture.provider.completePendingPublicAccountReads();
|
||||
QTRY_COMPARE(fixture.backend->assetStatus(), QStringLiteral("ready"));
|
||||
QCOMPARE(fixture.provider.publicAccountReadCalls, 3);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(AmmUiBackendDefinitionCacheTest)
|
||||
#include "AmmUiBackendDefinitionCacheTest.moc"
|
||||
@@ -0,0 +1,185 @@
|
||||
#include "FakeWalletProvider.h"
|
||||
#include "TokenDefinitionCache.h"
|
||||
|
||||
#include <QtTest>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
TokenDefinitionCacheKey cacheKey(const QString& fingerprint = QStringLiteral("channel:one"))
|
||||
{
|
||||
return {
|
||||
QStringLiteral("devnet"),
|
||||
fingerprint,
|
||||
QStringLiteral("http://127.0.0.1:8080"),
|
||||
{
|
||||
QString(64, QLatin1Char('a')),
|
||||
QString(64, QLatin1Char('b')),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
void makeReadsReady(FakeWalletProvider& provider)
|
||||
{
|
||||
provider.readResult.status = QStringLiteral("ok");
|
||||
provider.readResult.programOwner = QString(64, QLatin1Char('c'));
|
||||
provider.readResult.dataHex = QStringLiteral("00");
|
||||
}
|
||||
}
|
||||
|
||||
class TokenDefinitionCacheTest : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void reusesCompleteReads();
|
||||
void retriesIncompleteReads();
|
||||
void separatesNetworkKeys();
|
||||
void coalescesInFlightReads();
|
||||
void restartsCancelledRead();
|
||||
void dropsPendingCallbackOnDestruction();
|
||||
};
|
||||
|
||||
void TokenDefinitionCacheTest::reusesCompleteReads()
|
||||
{
|
||||
FakeWalletProvider provider;
|
||||
makeReadsReady(provider);
|
||||
TokenDefinitionCache cache(provider);
|
||||
const TokenDefinitionCacheKey key = cacheKey();
|
||||
|
||||
QVector<WalletAccountRead> first;
|
||||
cache.read(key, [&first](QVector<WalletAccountRead> reads) {
|
||||
first = std::move(reads);
|
||||
});
|
||||
|
||||
QCOMPARE(provider.publicAccountReadCalls, 1);
|
||||
QCOMPARE(provider.lastPublicAccountIds, key.tokenIds);
|
||||
QCOMPARE(first.size(), key.tokenIds.size());
|
||||
QVERIFY(cache.contains(key));
|
||||
|
||||
QVector<WalletAccountRead> second;
|
||||
cache.read(key, [&second](QVector<WalletAccountRead> reads) {
|
||||
second = std::move(reads);
|
||||
});
|
||||
|
||||
QCOMPARE(provider.publicAccountReadCalls, 1);
|
||||
QCOMPARE(second.size(), first.size());
|
||||
for (qsizetype index = 0; index < second.size(); ++index) {
|
||||
QCOMPARE(second.at(index).accountId, first.at(index).accountId);
|
||||
QCOMPARE(second.at(index).status, first.at(index).status);
|
||||
}
|
||||
}
|
||||
|
||||
void TokenDefinitionCacheTest::retriesIncompleteReads()
|
||||
{
|
||||
FakeWalletProvider provider;
|
||||
TokenDefinitionCache cache(provider);
|
||||
const TokenDefinitionCacheKey key = cacheKey();
|
||||
|
||||
cache.read(key, [](QVector<WalletAccountRead>) {});
|
||||
cache.read(key, [](QVector<WalletAccountRead>) {});
|
||||
|
||||
QCOMPARE(provider.publicAccountReadCalls, 2);
|
||||
QVERIFY(!cache.contains(key));
|
||||
}
|
||||
|
||||
void TokenDefinitionCacheTest::separatesNetworkKeys()
|
||||
{
|
||||
FakeWalletProvider provider;
|
||||
makeReadsReady(provider);
|
||||
TokenDefinitionCache cache(provider);
|
||||
const TokenDefinitionCacheKey baseKey = cacheKey();
|
||||
TokenDefinitionCacheKey fingerprintKey = baseKey;
|
||||
fingerprintKey.networkFingerprint = QStringLiteral("channel:two");
|
||||
TokenDefinitionCacheKey endpointKey = baseKey;
|
||||
endpointKey.sequencerAddress = QStringLiteral("http://127.0.0.1:8081");
|
||||
TokenDefinitionCacheKey definitionsKey = baseKey;
|
||||
definitionsKey.tokenIds = {
|
||||
baseKey.tokenIds.at(1),
|
||||
baseKey.tokenIds.at(0),
|
||||
};
|
||||
TokenDefinitionCacheKey networkKey = baseKey;
|
||||
networkKey.networkId = QStringLiteral("testnet");
|
||||
|
||||
cache.read(baseKey, [](QVector<WalletAccountRead>) {});
|
||||
cache.read(fingerprintKey, [](QVector<WalletAccountRead>) {});
|
||||
cache.read(endpointKey, [](QVector<WalletAccountRead>) {});
|
||||
cache.read(definitionsKey, [](QVector<WalletAccountRead>) {});
|
||||
cache.read(networkKey, [](QVector<WalletAccountRead>) {});
|
||||
|
||||
QCOMPARE(provider.publicAccountReadCalls, 5);
|
||||
QVERIFY(!cache.contains(baseKey));
|
||||
QVERIFY(cache.contains(networkKey));
|
||||
}
|
||||
|
||||
void TokenDefinitionCacheTest::coalescesInFlightReads()
|
||||
{
|
||||
FakeWalletProvider provider;
|
||||
makeReadsReady(provider);
|
||||
provider.deferPublicAccountReads = true;
|
||||
TokenDefinitionCache cache(provider);
|
||||
const TokenDefinitionCacheKey key = cacheKey();
|
||||
bool firstCalled = false;
|
||||
bool secondCalled = false;
|
||||
|
||||
cache.read(key, [&firstCalled](QVector<WalletAccountRead>) {
|
||||
firstCalled = true;
|
||||
});
|
||||
cache.read(key, [&secondCalled](QVector<WalletAccountRead>) {
|
||||
secondCalled = true;
|
||||
});
|
||||
|
||||
QCOMPARE(provider.publicAccountReadCalls, 1);
|
||||
provider.completePendingPublicAccountReads();
|
||||
|
||||
QVERIFY(firstCalled);
|
||||
QVERIFY(secondCalled);
|
||||
QVERIFY(cache.contains(key));
|
||||
}
|
||||
|
||||
void TokenDefinitionCacheTest::restartsCancelledRead()
|
||||
{
|
||||
FakeWalletProvider provider;
|
||||
makeReadsReady(provider);
|
||||
provider.deferPublicAccountReads = true;
|
||||
TokenDefinitionCache cache(provider);
|
||||
const TokenDefinitionCacheKey key = cacheKey();
|
||||
bool cancelledCallback = false;
|
||||
bool retryCallback = false;
|
||||
|
||||
cache.read(key, [&cancelledCallback](QVector<WalletAccountRead>) {
|
||||
cancelledCallback = true;
|
||||
});
|
||||
cache.cancelPending();
|
||||
cache.read(key, [&retryCallback](QVector<WalletAccountRead>) {
|
||||
retryCallback = true;
|
||||
});
|
||||
|
||||
QCOMPARE(provider.publicAccountReadCalls, 2);
|
||||
provider.completePendingPublicAccountReads();
|
||||
|
||||
QVERIFY(!cancelledCallback);
|
||||
QVERIFY(retryCallback);
|
||||
QVERIFY(cache.contains(key));
|
||||
}
|
||||
|
||||
void TokenDefinitionCacheTest::dropsPendingCallbackOnDestruction()
|
||||
{
|
||||
FakeWalletProvider provider;
|
||||
makeReadsReady(provider);
|
||||
provider.deferPublicAccountReads = true;
|
||||
const TokenDefinitionCacheKey key = cacheKey();
|
||||
bool callbackCalled = false;
|
||||
|
||||
{
|
||||
TokenDefinitionCache cache(provider);
|
||||
cache.read(key, [&callbackCalled](QVector<WalletAccountRead>) {
|
||||
callbackCalled = true;
|
||||
});
|
||||
}
|
||||
provider.completePendingPublicAccountReads();
|
||||
|
||||
QVERIFY(!callbackCalled);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TokenDefinitionCacheTest)
|
||||
#include "TokenDefinitionCacheTest.moc"
|
||||
Reference in New Issue
Block a user