Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/amm/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ tests/testnet/amm-tokens.json
# Isolated known-pools config written by tests/testnet/setup-amm-testnet.sh
tests/testnet/amm-pools.json

# Isolated single-file registry (AMM_REGISTRY_URL path) written by the setup script
tests/testnet/amm-registry.json

# Isolated custom-token store (CUSTOM_TOKEN_CONFIG) — initialized by the setup script
# and written by the app during tests/custom-token.mjs
tests/testnet/custom-tokens.json
3 changes: 3 additions & 0 deletions apps/amm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ logos_module(
src/AmmUiPlugin.cpp
src/AmmUiBackend.h
src/AmmUiBackend.cpp
src/RegistryLoader.h
src/RegistryLoader.cpp
FIND_PACKAGES
Qt6Gui
LINK_LIBRARIES
Qt6::Gui
Qt6::Network
LINK_TARGETS
logos_wallet_access
)
2 changes: 2 additions & 0 deletions apps/amm/qml/pages/LiquidityPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ onBackendChanged: { root.refreshHoldings(); root.refreshFeeTiers(); root.refresh
Connections {
target: root.backend
function onIsWalletOpenChanged() { root.refreshHoldings(); root.refreshTokens() }
// Re-fetch when the registry snapshot refreshes (e.g. a remote list lands).
function onRegistryRevisionChanged() { root.refreshTokens() }
}

readonly property int pageMargin: width < 640 ? 16 : 24
Expand Down
6 changes: 6 additions & 0 deletions apps/amm/qml/pages/PoolsPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ Item {
onBackendChanged: root.loadPools()
onRuntimeChanged: root.loadPools()

Connections {
target: root.backend
// Re-fetch when the registry snapshot refreshes (e.g. a remote list lands).
function onRegistryRevisionChanged() { root.loadPools() }
}

AmmTheme {
id: theme
}
Expand Down
14 changes: 11 additions & 3 deletions apps/amm/qml/pages/SwapPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,26 @@ Item {
})
}

function loadTokens() {
if (!root.backend)
return
logos.watch(root.backend.tokenList(),
function(list) { root.tokens = list },
function(err) { console.warn("tokenList error:", err) })
}

onBackendChanged: {
if (root.backend) {
logos.watch(root.backend.tokenList(),
function(list) { root.tokens = list },
function(err) { console.warn("tokenList error:", err) })
root.loadTokens()
root.refreshHoldings()
}
}

Connections {
target: root.backend
function onIsWalletOpenChanged() { root.refreshHoldings() }
// Re-fetch when the registry snapshot refreshes (e.g. a remote list lands).
function onRegistryRevisionChanged() { root.loadTokens() }
}

QtObject {
Expand Down
144 changes: 35 additions & 109 deletions apps/amm/src/AmmUiBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,123 +14,28 @@
#include <QTimer>

#include "LogosWalletProvider.h"
#include "RegistryLoader.h"
#include "WalletController.h"
#include "logos_api.h"
#include "logos_sdk.h"

namespace {
// Absolute path to the JSON known-pools config consumed by poolList().
// Mirrors TOKENS_CONFIG for the token list; produced by the AMM testnet
// setup script (apps/amm/tests/testnet/setup-amm-testnet.sh).
constexpr char POOLS_CONFIG_ENV[] = "AMM_POOLS_CONFIG";

// Parses the AMM_POOLS_CONFIG JSON file into the QVariantList the Pools UI
// renders. Fails soft (empty list) when the env var is unset, the file is
// unreadable, or the payload is not a JSON array — one malformed entry is
// skipped rather than dropping the whole list. tokenA/tokenB (display
// symbols) and a numeric feeBps are required; the id fields pass through
// when present so the entry can later be resolved on-chain.
QVariantList readPoolsConfig()
{
QVariantList out;

const QString path = qEnvironmentVariable(POOLS_CONFIG_ENV);
if (path.isEmpty())
return out;

QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return out;

const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isArray())
return out;

for (const QJsonValue& entry : doc.array()) {
if (!entry.isObject())
continue;
const QJsonObject obj = entry.toObject();

const QString tokenA = obj.value(QStringLiteral("tokenA")).toString();
const QString tokenB = obj.value(QStringLiteral("tokenB")).toString();
const QJsonValue feeBps = obj.value(QStringLiteral("feeBps"));
if (tokenA.isEmpty() || tokenB.isEmpty() || !feeBps.isDouble())
continue;

QVariantMap pool;
pool.insert(QStringLiteral("tokenA"), tokenA);
pool.insert(QStringLiteral("tokenB"), tokenB);
pool.insert(QStringLiteral("feeBps"), feeBps.toInt());
pool.insert(QStringLiteral("poolId"),
obj.value(QStringLiteral("poolId")).toString());
pool.insert(QStringLiteral("tokenADefinitionId"),
obj.value(QStringLiteral("tokenADefinitionId")).toString());
pool.insert(QStringLiteral("tokenBDefinitionId"),
obj.value(QStringLiteral("tokenBDefinitionId")).toString());
out.append(pool);
}
return out;
}

// Absolute path to the JSON token-list config consumed by tokenList().
constexpr char TOKENS_CONFIG_ENV[] = "TOKENS_CONFIG";

// Parses the TOKENS_CONFIG JSON file into the QVariantList the Swap token
// picker renders. Same fail-soft, skip-malformed-entry behavior as
// readPoolsConfig(). symbol/name are display; definitionId/holding are the
// token's account ids and pass through as configured (base58 or hex) — the
// module methods normalize to hex at their boundary. decimals must be a
// non-negative integer (a wrong value would misrender amounts).
QVariantList readTokensConfig()
{
QVariantList out;

const QString path = qEnvironmentVariable(TOKENS_CONFIG_ENV);
if (path.isEmpty())
return out;

QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return out;

const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isArray())
return out;

for (const QJsonValue& entry : doc.array()) {
if (!entry.isObject())
continue;
const QJsonObject obj = entry.toObject();

const QString definitionId = obj.value(QStringLiteral("definitionId")).toString();
const QString holding = obj.value(QStringLiteral("holding")).toString();
const QJsonValue decimals = obj.value(QStringLiteral("decimals"));
if (definitionId.isEmpty() || holding.isEmpty() || !decimals.isDouble())
continue;

QVariantMap token;
token.insert(QStringLiteral("symbol"), obj.value(QStringLiteral("symbol")).toString());
token.insert(QStringLiteral("name"), obj.value(QStringLiteral("name")).toString());
token.insert(QStringLiteral("definitionId"), definitionId);
token.insert(QStringLiteral("holding"), holding);
token.insert(QStringLiteral("decimals"), decimals.toInt());
out.append(token);
}
return out;
}
}


AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
: AmmUiBackendSimpleSource(parent),
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("amm_ui", this)),
m_logos(std::make_unique<LogosModules>(m_logosAPI)),
m_wallet(std::make_unique<LogosWalletProvider>(m_logosAPI)),
m_walletController(std::make_unique<WalletController>(
*m_wallet, QStringLiteral("AmmUI")))
*m_wallet, QStringLiteral("AmmUI"))),
m_registry(std::make_unique<RegistryLoader>())
{
setWalletStateReady(false);

// Bump registryRevision whenever the known-tokens / known-pools snapshot
// refreshes so QML replicas re-fetch tokenList()/poolList()/resolveTokens().
connect(m_registry.get(), &RegistryLoader::changed, this, [this]() {
setRegistryRevision(m_registry->revision());
});

connect(m_walletController.get(), &WalletController::stateChanged,
this, &AmmUiBackend::syncWalletState);
// Publishes an initial "loading" context (walletStateReady is still false,
Expand All @@ -140,6 +45,19 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent)
QTimer::singleShot(0, this, [this]() {
setWalletStateReady(true);
syncWalletState();
// Load the registry once the event loop is running (the remote source
// fetches asynchronously). Only the remote source needs the deployment
// guard, so skip the sequencer-touching configAccount read when local
// files are configured (they take precedence anyway).
if (!RegistryLoader::hasLocalSource()) {
const QVariantMap cfg = m_logos->amm_module.configAccount();
if (cfg.value(QStringLiteral("status")).toString() == QStringLiteral("ok")) {
m_registry->setConnectedProgramIds(
cfg.value(QStringLiteral("ammProgramId")).toString(),
cfg.value(QStringLiteral("tokenProgramId")).toString());
}
}
m_registry->refresh();
});
}

Expand Down Expand Up @@ -330,8 +248,16 @@ QVariantList AmmUiBackend::tokenList()
// Config-driven token list, read straight from TOKENS_CONFIG (like poolList
// reads AMM_POOLS_CONFIG). Token discovery is an app concern, so this stays
// in the backend rather than the amm_module; the swap/quote module methods
// normalize the ids (base58 or hex) at their boundary.
return readTokensConfig();
// normalize the ids (base58 or hex) at their boundary. Served from the
// RegistryLoader snapshot (re-fetched when registryRevision changes).
return m_registry->tokens();
}

void AmmUiBackend::refreshRegistry()
{
// Manual re-load of the known-tokens/known-pools source. The loader bumps
// registryRevision and the UI re-fetches the lists.
m_registry->refresh();
}

QVariantMap AmmUiBackend::createPoolQuote(QVariantMap request)
Expand Down Expand Up @@ -367,8 +293,8 @@ QVariantList AmmUiBackend::poolList()
// Config-driven known pools. Read straight from AMM_POOLS_CONFIG on every
// call (the UI fetches this once on load); adding more pairs is a config
// edit, no app change. Pool discovery is an app concern, so this stays in
// the backend rather than the amm_module.
return readPoolsConfig();
// the backend rather than the amm_module. Served from the RegistryLoader snapshot.
return m_registry->pools();
}

QVariantList AmmUiBackend::feeTiers()
Expand All @@ -388,7 +314,7 @@ QVariantList AmmUiBackend::resolveTokens()
const bool wallet_open = isWalletOpen();

QVariantList ids;
const QVariantList configured = readTokensConfig();
const QVariantList configured = m_registry->tokens();
for (const QVariant& entry : configured) {
const QString id = entry.toMap().value(QStringLiteral("definitionId")).toString();
if (!id.isEmpty())
Expand Down
5 changes: 5 additions & 0 deletions apps/amm/src/AmmUiBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class LogosAPI;
struct LogosModules;
class LogosWalletProvider;
class WalletController;
class RegistryLoader;

// Source-side implementation of the AmmUiBackend .rep interface.
// Inheriting from AmmUiBackendSimpleSource gives us the generated PROPs and
Expand Down Expand Up @@ -101,6 +102,8 @@ public slots:
QVariantList resolveTokens() override;
// Validates + persists a user-pasted custom token id (see the .rep).
QVariantMap addCustomToken(QString tokenId) override;
// Re-loads the known-tokens / known-pools registry (bumps registryRevision).
void refreshRegistry() override;

private:
void syncWalletState();
Expand All @@ -120,6 +123,8 @@ public slots:
std::unique_ptr<LogosModules> m_logos;
std::unique_ptr<LogosWalletProvider> m_wallet;
std::unique_ptr<WalletController> m_walletController;
// Known-tokens / known-pools snapshot source (local files now; remote later).
std::unique_ptr<RegistryLoader> m_registry;
};

#endif // AMM_UI_BACKEND_H
9 changes: 9 additions & 0 deletions apps/amm/src/AmmUiBackend.rep
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class AmmUiBackend
// Whether the configured sequencer answered the last reachability probe.
// Defaults true so the UI doesn't flash a warning before the first check.
PROP(bool sequencerReachable READONLY)
// Bumps on every known-tokens / known-pools registry refresh so QML replicas
// re-fetch tokenList()/poolList()/resolveTokens(). Starts at 0; the backend
// sets it once the initial snapshot has loaded (and again on each refresh).
PROP(int registryRevision READONLY)

// Account management
SLOT(QString createAccountPublic())
Expand Down Expand Up @@ -201,4 +205,9 @@ class AmmUiBackend
// and returns { ok: true, token: <row> } with the resolved row; on an unresolvable /
// non-fungible id returns { ok: false, error: "unresolved" } and persists nothing.
SLOT(QVariantMap addCustomToken(QString tokenId))

// Re-loads the known-tokens / known-pools source (the local files, or later
// a remote registry). Bumps registryRevision when the snapshot updates so
// QML re-fetches the lists.
SLOT(void refreshRegistry())
}
Loading
Loading