mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
perf(amm): cache verified token definitions
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user