perf(amm): cache verified token definitions

This commit is contained in:
Ricardo Guilherme Schmidt
2026-07-17 21:39:09 -03:00
parent 6ccd37b231
commit b71be34edf
8 changed files with 744 additions and 15 deletions
+43
View File
@@ -33,6 +33,8 @@ logos_module(
src/AmmUiBackend.cpp
src/ActiveNetwork.h
src/ActiveNetwork.cpp
src/TokenDefinitionCache.h
src/TokenDefinitionCache.cpp
src/WalletIdlDecoder.h
src/WalletIdlDecoder.cpp
FIND_PACKAGES
@@ -74,4 +76,45 @@ if(BUILD_TESTING)
target_include_directories(amm_active_network_test PRIVATE src)
target_link_libraries(amm_active_network_test PRIVATE Qt6::Core Qt6::Test)
add_test(NAME amm_active_network COMMAND amm_active_network_test)
add_executable(amm_token_definition_cache_test
tests/cpp/TokenDefinitionCacheTest.cpp
src/TokenDefinitionCache.h
src/TokenDefinitionCache.cpp
)
set_target_properties(amm_token_definition_cache_test PROPERTIES AUTOMOC ON)
target_compile_features(amm_token_definition_cache_test PRIVATE cxx_std_17)
target_include_directories(amm_token_definition_cache_test PRIVATE
src
"${LOGOS_WALLET_SOURCE_DIR}/src"
"${LOGOS_WALLET_SOURCE_DIR}/tests/support"
)
target_link_libraries(amm_token_definition_cache_test PRIVATE Qt6::Core Qt6::Test)
add_test(NAME amm_token_definition_cache COMMAND amm_token_definition_cache_test)
add_executable(amm_backend_definition_cache_test
tests/cpp/AmmUiBackendDefinitionCacheTest.cpp
)
add_dependencies(amm_backend_definition_cache_test amm_ui_module_plugin)
set_target_properties(amm_backend_definition_cache_test PROPERTIES AUTOMOC ON)
target_compile_features(amm_backend_definition_cache_test PRIVATE cxx_std_17)
target_include_directories(amm_backend_definition_cache_test PRIVATE
src
"${CMAKE_CURRENT_BINARY_DIR}"
"${LOGOS_WALLET_SOURCE_DIR}/src"
"${LOGOS_WALLET_SOURCE_DIR}/tests/support"
)
target_link_libraries(amm_backend_definition_cache_test PRIVATE
Qt6::Core
Qt6::Network
Qt6::RemoteObjects
Qt6::Test
amm_ui_module_plugin
)
if(UNIX AND NOT APPLE)
target_link_options(amm_backend_definition_cache_test PRIVATE
"-Wl,--allow-shlib-undefined"
)
endif()
add_test(NAME amm_backend_definition_cache COMMAND amm_backend_definition_cache_test)
endif()
+66 -5
View File
@@ -109,12 +109,33 @@ WalletAccountRead accountRead(const WalletAccount& account)
AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
: AmmUiBackendSimpleSource(parent),
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
m_wallet(std::make_unique<LogosWalletProvider>(m_logosAPI)),
m_ownedWallet(std::make_unique<LogosWalletProvider>(m_logosAPI)),
m_wallet(m_ownedWallet.get()),
m_definitionCache(*m_wallet),
m_walletController(std::make_unique<WalletController>(
*m_wallet, QStringLiteral("AmmUI"))),
m_networkManager(new QNetworkAccessManager(this)),
m_tokenIdl(resource(QStringLiteral(":/amm/idl/token-idl.json"))),
m_ammIdl(resource(QStringLiteral(":/amm/idl/amm-idl.json")))
{
initialize();
}
AmmUiBackend::AmmUiBackend(WalletProvider& wallet, QObject* parent)
: AmmUiBackendSimpleSource(parent),
m_logosAPI(nullptr),
m_wallet(&wallet),
m_definitionCache(*m_wallet),
m_walletController(std::make_unique<WalletController>(
*m_wallet, QStringLiteral("AmmUI"))),
m_networkManager(new QNetworkAccessManager(this)),
m_tokenIdl(resource(QStringLiteral(":/amm/idl/token-idl.json"))),
m_ammIdl(resource(QStringLiteral(":/amm/idl/amm-idl.json")))
{
initialize();
}
void AmmUiBackend::initialize()
{
setAssets({});
setAssetStatus(QStringLiteral("idle"));
@@ -198,6 +219,10 @@ bool AmmUiBackend::setPrimaryAccount(QString accountId)
void AmmUiBackend::syncWalletState()
{
const WalletUiState& state = m_walletController->state();
if (state.syncStatus == QStringLiteral("opening")
|| state.syncStatus == QStringLiteral("syncing")) {
m_definitionCache.cancelPending();
}
const QString previousAddress = sequencerAddr();
const bool wasReachable = sequencerReachable();
setIsWalletOpen(state.isWalletOpen);
@@ -274,41 +299,71 @@ void AmmUiBackend::refreshPortfolio()
{
const quint64 generation = ++m_portfolioGeneration;
if (!m_walletController->state().isWalletOpen) {
m_definitionCache.cancelPending();
setAssets({});
setAssetStatus(QStringLiteral("idle"));
setAssetError({});
return;
}
if (m_network.status() != QStringLiteral("ready")) {
invalidateDefinitionCache();
setAssets({});
setAssetStatus(QStringLiteral("blocked"));
setAssetError(m_network.status());
return;
}
if (m_tokenIdl.isEmpty()) {
invalidateDefinitionCache();
setAssetStatus(QStringLiteral("error"));
setAssetError(QStringLiteral("token_idl_missing"));
return;
}
const TokenDefinitionCacheKey key = definitionCacheKey(m_network.snapshot());
setAssetStatus(QStringLiteral("loading"));
setAssetError({});
m_wallet->readPublicAccountsAsync(
m_network.snapshot().tokenIds,
[this, generation](QVector<WalletAccountRead> reads) {
applyDefinitions(generation, reads);
if (m_appliedDefinitionKey && *m_appliedDefinitionKey == key
&& m_definitionCache.contains(key)) {
applyWalletPortfolio(generation);
return;
}
m_definitionCache.read(
key,
[this, generation, key](QVector<WalletAccountRead> reads) {
applyDefinitions(generation, key, reads);
});
}
TokenDefinitionCacheKey AmmUiBackend::definitionCacheKey(
const ActiveNetworkSnapshot& network) const
{
return {
network.id,
network.fingerprint,
sequencerAddr(),
network.tokenIds,
};
}
void AmmUiBackend::invalidateDefinitionCache()
{
m_definitionCache.clear();
m_appliedDefinitionKey.reset();
}
void AmmUiBackend::applyDefinitions(
quint64 generation,
const TokenDefinitionCacheKey& key,
const QVector<WalletAccountRead>& reads)
{
if (generation != m_portfolioGeneration)
return;
const ActiveNetworkSnapshot network = m_network.snapshot();
if (!(key == definitionCacheKey(network)))
return;
const WalletDecodeResult decoded = WalletIdlDecoder::decode(m_tokenIdl, reads);
if (!decoded.ok() || reads.size() != network.tokenIds.size()
|| decoded.accounts.size() != reads.size()) {
invalidateDefinitionCache();
setAssetStatus(QStringLiteral("error"));
setAssetError(decoded.error.isEmpty()
? QStringLiteral("definition_decode_failed")
@@ -338,6 +393,7 @@ void AmmUiBackend::applyDefinitions(
if (m_tokenProgramId.isEmpty())
m_tokenProgramId = read.programOwner;
else if (m_tokenProgramId != read.programOwner) {
invalidateDefinitionCache();
setAssets({});
setAssetStatus(QStringLiteral("error"));
setAssetError(QStringLiteral("token_program_mismatch"));
@@ -349,6 +405,7 @@ void AmmUiBackend::applyDefinitions(
m_tokens.append(std::move(token));
}
if (m_tokenProgramId.isEmpty()) {
invalidateDefinitionCache();
setAssets({});
setAssetStatus(QStringLiteral("error"));
setAssetError(QStringLiteral("definitions_unavailable"));
@@ -356,6 +413,10 @@ void AmmUiBackend::applyDefinitions(
}
m_idlRegistry.registerProgram(
m_tokenProgramId, QStringLiteral("Token"), m_tokenIdl);
if (unavailable > 0)
invalidateDefinitionCache();
else
m_appliedDefinitionKey = key;
setAssetError(unavailable > 0
? QStringLiteral("some_definitions_unavailable")
: QString());
+13 -1
View File
@@ -2,6 +2,7 @@
#define AMM_UI_BACKEND_H
#include <memory>
#include <optional>
#include <QHash>
#include <QString>
@@ -10,6 +11,7 @@
#include "rep_AmmUiBackend_source.h"
#include "ActiveNetwork.h"
#include "TokenDefinitionCache.h"
#include "WalletAccountModel.h"
#include "WalletIdlDecoder.h"
@@ -24,6 +26,8 @@ class AmmUiBackend : public AmmUiBackendSimpleSource {
public:
explicit AmmUiBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
// The injected provider must outlive the backend.
explicit AmmUiBackend(WalletProvider& wallet, QObject* parent = nullptr);
~AmmUiBackend() override;
WalletAccountModel* accountModel() const;
@@ -51,14 +55,21 @@ private:
void syncWalletState();
void publishNetworkState();
void initialize();
void probeNetworkIdentity();
void refreshPortfolio();
TokenDefinitionCacheKey definitionCacheKey(
const ActiveNetworkSnapshot& network) const;
void invalidateDefinitionCache();
void applyDefinitions(quint64 generation,
const TokenDefinitionCacheKey& key,
const QVector<WalletAccountRead>& reads);
void applyWalletPortfolio(quint64 generation);
LogosAPI* m_logosAPI;
std::unique_ptr<LogosWalletProvider> m_wallet;
std::unique_ptr<LogosWalletProvider> m_ownedWallet;
WalletProvider* m_wallet;
TokenDefinitionCache m_definitionCache;
std::unique_ptr<WalletController> m_walletController;
QNetworkAccessManager* m_networkManager;
ActiveNetwork m_network;
@@ -67,6 +78,7 @@ private:
WalletIdlRegistry m_idlRegistry;
QVector<TokenInfo> m_tokens;
QString m_tokenProgramId;
std::optional<TokenDefinitionCacheKey> m_appliedDefinitionKey;
bool m_identityProbeInFlight = false;
quint64 m_portfolioGeneration = 0;
};
+104
View File
@@ -0,0 +1,104 @@
#include "TokenDefinitionCache.h"
#include <utility>
bool TokenDefinitionCacheKey::isReusable() const
{
return !networkId.isEmpty()
&& !networkFingerprint.isEmpty()
&& !sequencerAddress.isEmpty()
&& !tokenIds.isEmpty();
}
bool TokenDefinitionCacheKey::operator==(const TokenDefinitionCacheKey& other) const
{
return networkId == other.networkId
&& networkFingerprint == other.networkFingerprint
&& sequencerAddress == other.sequencerAddress
&& tokenIds == other.tokenIds;
}
TokenDefinitionCache::TokenDefinitionCache(WalletProvider& provider)
: m_provider(provider),
m_state(std::make_shared<State>())
{
}
TokenDefinitionCache::~TokenDefinitionCache()
{
clear();
}
void TokenDefinitionCache::read(const TokenDefinitionCacheKey& key, Callback callback)
{
if (contains(key)) {
callback(m_state->cachedReads);
return;
}
if (m_state->inFlight && m_state->inFlight->key == key) {
m_state->inFlight->callbacks.append(std::move(callback));
return;
}
cancelPending();
const auto request = std::make_shared<InFlight>();
request->key = key;
request->callbacks.append(std::move(callback));
m_state->inFlight = request;
const std::weak_ptr<State> state = m_state;
m_provider.readPublicAccountsAsync(
key.tokenIds,
[state, request](QVector<WalletAccountRead> reads) mutable {
const std::shared_ptr<State> lockedState = state.lock();
if (!lockedState || request->cancelled)
return;
if (lockedState->inFlight == request) {
lockedState->inFlight.reset();
if (TokenDefinitionCache::isComplete(request->key, reads)) {
lockedState->cachedKey = request->key;
lockedState->cachedReads = reads;
}
}
QVector<Callback> callbacks = std::move(request->callbacks);
for (Callback& callback : callbacks)
callback(reads);
});
}
bool TokenDefinitionCache::contains(const TokenDefinitionCacheKey& key) const
{
return m_state->cachedKey && *m_state->cachedKey == key;
}
void TokenDefinitionCache::cancelPending()
{
if (m_state->inFlight) {
m_state->inFlight->cancelled = true;
m_state->inFlight->callbacks.clear();
}
m_state->inFlight.reset();
}
void TokenDefinitionCache::clear()
{
m_state->cachedKey.reset();
m_state->cachedReads.clear();
cancelPending();
}
bool TokenDefinitionCache::isComplete(
const TokenDefinitionCacheKey& key,
const QVector<WalletAccountRead>& reads)
{
if (!key.isReusable() || reads.size() != key.tokenIds.size())
return false;
for (qsizetype index = 0; index < reads.size(); ++index) {
const WalletAccountRead& read = reads.at(index);
if (!read.ok() || read.accountId != key.tokenIds.at(index)
|| read.programOwner.isEmpty() || read.dataHex.isEmpty()) {
return false;
}
}
return true;
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <functional>
#include <memory>
#include <optional>
#include <QString>
#include <QStringList>
#include <QVector>
#include "WalletProvider.h"
struct TokenDefinitionCacheKey {
QString networkId;
QString networkFingerprint;
QString sequencerAddress;
QStringList tokenIds;
bool isReusable() const;
bool operator==(const TokenDefinitionCacheKey& other) const;
};
class TokenDefinitionCache final {
public:
using Callback = std::function<void(QVector<WalletAccountRead>)>;
explicit TokenDefinitionCache(WalletProvider& provider);
~TokenDefinitionCache();
void read(const TokenDefinitionCacheKey& key, Callback callback);
bool contains(const TokenDefinitionCacheKey& key) const;
void cancelPending();
void clear();
private:
struct InFlight {
TokenDefinitionCacheKey key;
QVector<Callback> callbacks;
bool cancelled = false;
};
struct State {
std::optional<TokenDefinitionCacheKey> cachedKey;
QVector<WalletAccountRead> cachedReads;
std::shared_ptr<InFlight> inFlight;
};
static bool isComplete(const TokenDefinitionCacheKey& key,
const QVector<WalletAccountRead>& reads);
WalletProvider& m_provider;
std::shared_ptr<State> m_state;
};
@@ -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"
@@ -20,12 +20,21 @@ public:
int clearCalls = 0;
int createAccountCalls = 0;
mutable int readCalls = 0;
int publicAccountReadCalls = 0;
int submitCalls = 0;
int disconnectCalls = 0;
bool lastForceRefresh = false;
bool lastAccountWasPublic = false;
WalletPaths lastPaths;
WalletTransaction lastTransaction;
QStringList lastPublicAccountIds;
bool deferPublicAccountReads = false;
struct PendingPublicAccountRead {
QStringList accountIds;
AccountReadsCallback callback;
};
QVector<PendingPublicAccountRead> pendingPublicAccountReads;
WalletSession connect(const WalletPaths& paths) override
{
@@ -83,16 +92,21 @@ public:
void readPublicAccountsAsync(const QStringList& accountIds,
AccountReadsCallback callback) override
{
QVector<WalletAccountRead> results = readResults;
if (results.isEmpty()) {
results.reserve(accountIds.size());
for (const QString& accountId : accountIds) {
WalletAccountRead result = readResult;
result.accountId = accountId;
results.append(std::move(result));
}
++publicAccountReadCalls;
lastPublicAccountIds = accountIds;
if (deferPublicAccountReads) {
pendingPublicAccountReads.append({ accountIds, std::move(callback) });
return;
}
callback(std::move(results));
callback(accountReads(accountIds));
}
void completePendingPublicAccountReads()
{
QVector<PendingPublicAccountRead> pending;
pending.swap(pendingPublicAccountReads);
for (PendingPublicAccountRead& read : pending)
read.callback(accountReads(read.accountIds));
}
WalletSubmission submitPublicTransaction(
@@ -104,4 +118,19 @@ public:
}
void disconnect() override { ++disconnectCalls; }
private:
QVector<WalletAccountRead> accountReads(const QStringList& accountIds) const
{
QVector<WalletAccountRead> results = readResults;
if (results.isEmpty()) {
results.reserve(accountIds.size());
for (const QString& accountId : accountIds) {
WalletAccountRead result = readResult;
result.accountId = accountId;
results.append(std::move(result));
}
}
return results;
}
};