refactor(amm): move tokenList off the module; app reads TOKENS_CONFIG

Token discovery is an app concern, not module business — same rationale as
poolList reading AMM_POOLS_CONFIG. Drop tokenList() from amm_module and have the
app read the config itself.
This commit is contained in:
r4bbit
2026-08-14 11:45:07 +02:00
parent 7e45e44eac
commit d48900e931
6 changed files with 121 additions and 103 deletions
+53 -2
View File
@@ -71,6 +71,53 @@ namespace {
}
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;
}
}
@@ -280,7 +327,11 @@ QString AmmUiBackend::swapExactOutput(QString defAHex, QString defBHex, QString
QVariantList AmmUiBackend::tokenList()
{
return m_logos->amm_module.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();
}
QVariantMap AmmUiBackend::createPoolQuote(QVariantMap request)
@@ -330,7 +381,7 @@ QVariantList AmmUiBackend::resolveTokens()
const bool wallet_open = isWalletOpen();
QVariantList ids;
const QVariantList configured = m_logos->amm_module.tokenList();
const QVariantList configured = readTokensConfig();
for (const QVariant& entry : configured) {
const QString id = entry.toMap().value(QStringLiteral("definitionId")).toString();
if (!id.isEmpty())
+3 -3
View File
@@ -69,8 +69,8 @@ public slots:
QString swapExactOutput(QString defAHex, QString defBHex, QString userInputHoldingHex,
QString userOutputHoldingHex, QString amountOutDecimal,
QString maxInDecimal, QString deadlineDecimal) override;
// Reads the token list from TOKENS_CONFIG (via the module) so the Swap UI's
// token picker is config-driven instead of hardcoded.
// Reads the token list from TOKENS_CONFIG app-side (like poolList reads
// AMM_POOLS_CONFIG) so the Swap UI's token picker is config-driven.
QVariantList tokenList() override;
// Create-pool preview (createPoolQuote, read-only) and submit (createPool). The caller
// supplies lpHoldingId in the request — a fresh account it created via
@@ -107,7 +107,7 @@ private:
LogosAPI* m_logosAPI;
// Handle for the amm_module core module (resolvePool / swapExactInput /
// tokenList / resolveTokens). The module wraps the amm_ffi brain and
// resolveTokens). The module wraps the amm_ffi brain and
// reaches the shared wallet through its own logos_execution_zone dependency;
// this backend keeps a thin LogosModules over the same LogosAPI as the
// wallet provider so both resolve that one shared wallet instance.
+4 -2
View File
@@ -100,8 +100,10 @@ class AmmUiBackend
SLOT(QString swapExactOutput(QString defAHex, QString defBHex, QString userInputHoldingHex, QString userOutputHoldingHex, QString amountOutDecimal, QString maxInDecimal, QString deadlineDecimal))
// Reads the token list config at TOKENS_CONFIG (absolute path, JSON array
// of { symbol, name, definitionId, holding, decimals }) and returns it as
// a QVariantList of QVariantMap entries. Returns an empty list if
// TOKENS_CONFIG is unset/unreadable/invalid.
// a QVariantList of QVariantMap entries. Read app-side (like poolList); ids
// pass through as configured (base58 or hex) and the swap/quote module
// methods normalize them. Returns an empty list if TOKENS_CONFIG is
// unset/unreadable/invalid.
SLOT(QVariantList tokenList())
// Server-side create-pool preview from the two deposit amounts. `request`
+11 -24
View File
@@ -27,8 +27,6 @@ methods (the module API is generated from the header) are:
— submits an on-chain `SwapExactInput` transaction (defA = token in,
defB = token out); returns the tx hash (or empty on failure). See
**Amount / id conventions** below.
- `tokenList()` — reads the `TOKENS_CONFIG` JSON array and returns it with
`definitionId`/`holding` normalized to hex.
- `resolveTokens(request, walletOpen)` — resolves an app-provided set of token
ids into selector rows (definition + wallet holding per id). The lean,
stateless successor to the removed `newPositionContext` path: the app owns the
@@ -67,9 +65,9 @@ The impl is deliberately **Qt-free** (`std::string` / `LogosMap` / `LogosList` /
## Amount / id conventions
**Account ids are hex**, not base58. The `*Hex` args are parsed as 32-byte hex;
a base58 id (what the wallet/runbook display) fails that parse. Convert with
`tokenList()` (it emits hex) or `logos_execution_zone.account_id_from_base58 <base58>`.
**Account ids accept base58 or hex.** The `*Hex` args and request-map ids are
normalized at each method's boundary (via `logos_execution_zone.account_id_from_base58`
for base58 inputs), so the wallet/runbook's base58 ids can be passed directly.
**Amounts (`amountIn`/`minOut`, u128) and `deadline` (u64 unix-ms)** are declared
`nlohmann::json`, so each accepts **either a JSON number or a decimal string**:
@@ -104,8 +102,9 @@ Both are absolute-path env vars set on the **process that hosts the module**
- `AMM_PROGRAM_BIN` — the deployed `amm.bin`. Required; its ELF determines the
program id and every derived PDA. Without it, `resolvePoolAccount` returns
`{ status: "error", error: "no_program_bin" }`.
- `TOKENS_CONFIG` — JSON array of `{ symbol, name, definitionId, holding, decimals }`
consumed by `tokenList()`.
(The token list config `TOKENS_CONFIG` is an **app** concern now — the module no
longer reads it; see `apps/amm/README.md`.)
## Headless usage with `logoscore`
@@ -145,18 +144,13 @@ Have all of the following in place before staging the modules dir:
your target sequencer (its ELF fixes the program id and every PDA). See
`apps/amm/README.md` and the testnet runbook.
6. **A tokens config** for `TOKENS_CONFIG` — a JSON array of
`{ symbol, name, definitionId, holding, decimals }` (e.g. the repo's
`amm-tokens.json`).
7. **A wallet** at `~/.lee/wallet` (`wallet_config.json` with `sequencer_addr`
6. **A wallet** at `~/.lee/wallet` (`wallet_config.json` with `sequencer_addr`
pointing at your sequencer, plus `storage.json` with your accounts), and a
**running sequencer** with the AMM initialized and a pool holding liquidity.
`tokenList` reads `TOKENS_CONFIG` from disk and needs no wallet, but every
other op (including `resolvePool`) reads on-chain through the wallet module's
`get_account_public`, which needs the wallet **open** (the handle is null
until `open`/`create_new`); `swapExactInput` additionally needs it **synced**
(see below).
Every op (including `resolvePoolAccount`) reads on-chain through the wallet
module's `get_account_public`, which needs the wallet **open** (the handle is
null until `open`/`create_new`); `swapExactInput` additionally needs it
**synced** (see below).
### Staging the modules directory
@@ -195,19 +189,12 @@ first, then the module:
```bash
AMM_PROGRAM_BIN=/abs/path/to/amm.bin \
TOKENS_CONFIG=/abs/path/to/amm-tokens.json \
logoscore -D -m ./modules --persistence-path ./data
logoscore load-module logos_execution_zone # dependency first
logoscore load-module amm_module
```
`tokenList` reads `TOKENS_CONFIG` from disk — no wallet needed:
```bash
logoscore call amm_module tokenList
```
Every other op reads on-chain through the wallet module's `get_account_public`,
which fails on a null wallet handle (surfacing as an absent pool), so open the
wallet first — `resolvePool` then works:
+50 -66
View File
@@ -43,9 +43,6 @@ bool ammDebug() {
// what determine the program id (and every PDA derived from it).
constexpr char AMM_PROGRAM_BIN_ENV[] = "AMM_PROGRAM_BIN";
// Absolute path to the JSON token-list config consumed by tokenList().
constexpr char TOKENS_CONFIG_ENV[] = "TOKENS_CONFIG";
int hexVal(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
@@ -576,12 +573,19 @@ LogosMap AmmModuleImpl::swapExactInQuote(const std::string& token_in_hex,
if (amm_program_id.empty())
return error("config_missing");
// tokenList() moved app-side, so token ids arrive as configured (base58 or
// hex); normalize to the hex the FFI expects.
const std::string token_in = normalizeAccountId(token_in_hex);
const std::string token_out = normalizeAccountId(token_out_hex);
if (token_in.empty() || token_out.empty())
return error("invalid_token_id");
// Derive the pool id (config-free) and read the pool account; its raw data is
// handed to the pricing op. An absent account has no data → `no_pool`.
const FfiResult poolId = call(amm_pool_id, json{
{"ammProgramId", amm_program_id},
{"tokenInId", token_in_hex},
{"tokenOutId", token_out_hex},
{"tokenInId", token_in},
{"tokenOutId", token_out},
});
if (!poolId.ok)
return error(poolId.error.empty() ? "backend_error" : poolId.error);
@@ -589,8 +593,8 @@ LogosMap AmmModuleImpl::swapExactInQuote(const std::string& token_in_hex,
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
const FfiResult quoteResult = call(amm_swap_exact_in_quote, json{
{"tokenInId", token_in_hex},
{"tokenOutId", token_out_hex},
{"tokenInId", token_in},
{"tokenOutId", token_out},
{"amountInRaw", amount_in_decimal},
{"slippageBps", slippage_bps},
{"poolData", pool_data},
@@ -622,12 +626,17 @@ LogosMap AmmModuleImpl::swapExactOutQuote(const std::string& token_in_hex,
if (amm_program_id.empty())
return error("config_missing");
// tokenList() moved app-side, so token ids arrive as configured (base58 or
// hex); normalize to the hex the FFI expects.
const std::string token_in = normalizeAccountId(token_in_hex);
const std::string token_out = normalizeAccountId(token_out_hex);
// Derive the pool id (config-free) and read the pool account; its raw data is
// handed to the pricing op. An absent account has no data → `no_pool`.
const FfiResult poolId = call(amm_pool_id, json{
{"ammProgramId", amm_program_id},
{"tokenInId", token_in_hex},
{"tokenOutId", token_out_hex},
{"tokenInId", token_in},
{"tokenOutId", token_out},
});
if (!poolId.ok)
return error(poolId.error.empty() ? "backend_error" : poolId.error);
@@ -635,8 +644,8 @@ LogosMap AmmModuleImpl::swapExactOutQuote(const std::string& token_in_hex,
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
const FfiResult quoteResult = call(amm_swap_exact_out_quote, json{
{"tokenInId", token_in_hex},
{"tokenOutId", token_out_hex},
{"tokenInId", token_in},
{"tokenOutId", token_out},
{"amountOutRaw", amount_out_decimal},
{"slippageBps", slippage_bps},
{"poolData", pool_data},
@@ -680,12 +689,23 @@ std::string AmmModuleImpl::swapExactInput(const std::string& def_a_hex,
return {};
}
// tokenList() moved app-side, so token/holding ids arrive as configured
// (base58 or hex); normalize to the hex the FFI expects.
const std::string def_a = normalizeAccountId(def_a_hex);
const std::string def_b = normalizeAccountId(def_b_hex);
const std::string input_holding = normalizeAccountId(user_input_holding_hex);
const std::string output_holding = normalizeAccountId(user_output_holding_hex);
if (def_a.empty() || def_b.empty() || input_holding.empty() || output_holding.empty()) {
AMM_TRACE("swapExactInput: FAIL invalid token/holding id");
return {};
}
// Read the pool so the plan can use its stored vault ids (the guest asserts
// the vaults in the pool's creation order — see amm_swap_exact_in_plan).
const FfiResult poolId = call(amm_pool_id, json{
{"ammProgramId", amm_program_id},
{"tokenInId", def_a_hex},
{"tokenOutId", def_b_hex},
{"tokenInId", def_a},
{"tokenOutId", def_b},
});
if (!poolId.ok) {
AMM_TRACE("swapExactInput: FAIL amm_pool_id");
@@ -698,12 +718,12 @@ std::string AmmModuleImpl::swapExactInput(const std::string& def_a_hex,
// and returns a ready-to-submit plan.
const FfiResult planResult = call(amm_swap_exact_in_plan, json{
{"ammProgramId", amm_program_id},
{"tokenInId", def_a_hex},
{"tokenOutId", def_b_hex},
{"tokenInId", def_a},
{"tokenOutId", def_b},
{"config", config},
{"poolData", pool_data},
{"userInputHoldingId", user_input_holding_hex},
{"userOutputHoldingId", user_output_holding_hex},
{"userInputHoldingId", input_holding},
{"userOutputHoldingId", output_holding},
{"amountIn", amount_in_decimal},
{"minOut", min_out_decimal},
{"deadlineMs", deadline_decimal},
@@ -763,12 +783,19 @@ std::string AmmModuleImpl::swapExactOutput(const std::string& def_a_hex,
return {};
}
// tokenList() moved app-side, so token/holding ids arrive as configured
// (base58 or hex); normalize to the hex the FFI expects.
const std::string def_a = normalizeAccountId(def_a_hex);
const std::string def_b = normalizeAccountId(def_b_hex);
const std::string input_holding = normalizeAccountId(user_input_holding_hex);
const std::string output_holding = normalizeAccountId(user_output_holding_hex);
// Read the pool so the plan can use its stored vault ids (the guest asserts
// the vaults in the pool's creation order — see amm_swap_exact_out_plan).
const FfiResult poolId = call(amm_pool_id, json{
{"ammProgramId", amm_program_id},
{"tokenInId", def_a_hex},
{"tokenOutId", def_b_hex},
{"tokenInId", def_a},
{"tokenOutId", def_b},
});
if (!poolId.ok) {
AMM_TRACE("swapExactOutput: FAIL amm_pool_id");
@@ -781,12 +808,12 @@ std::string AmmModuleImpl::swapExactOutput(const std::string& def_a_hex,
// and returns a ready-to-submit plan.
const FfiResult planResult = call(amm_swap_exact_out_plan, json{
{"ammProgramId", amm_program_id},
{"tokenInId", def_a_hex},
{"tokenOutId", def_b_hex},
{"tokenInId", def_a},
{"tokenOutId", def_b},
{"config", config},
{"poolData", pool_data},
{"userInputHoldingId", user_input_holding_hex},
{"userOutputHoldingId", user_output_holding_hex},
{"userInputHoldingId", input_holding},
{"userOutputHoldingId", output_holding},
{"amountOut", amount_out_decimal},
{"maxIn", max_in_decimal},
{"deadlineMs", deadline_decimal},
@@ -1324,49 +1351,6 @@ LogosMap AmmModuleImpl::syncReserves(const LogosMap& request) {
return LogosMap{{"status", "ok"}, {"error", ""}, {"transactionId", jStr(obj, "tx_hash")}};
}
LogosList AmmModuleImpl::tokenList() {
LogosList out = LogosList::array();
const char* path = std::getenv(TOKENS_CONFIG_ENV);
if (path == nullptr || *path == '\0') return out;
std::ifstream file(path);
if (!file) return out;
const std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
const auto arr = json::parse(content, nullptr, /*allow_exceptions=*/false);
if (!arr.is_array()) return out;
for (const auto& entry : arr) {
if (!entry.is_object()) continue;
// definitionId/holding may be base58 or hex — normalize both to
// lowercase hex so downstream consumers can assume hex.
const std::string definition_id = normalizeAccountId(jStr(entry, "definitionId"));
const std::string holding = normalizeAccountId(jStr(entry, "holding"));
if (definition_id.empty() || holding.empty()) continue;
// decimals must be a non-negative integer. A present-but-non-integer
// value (e.g. "decimals": "18") would make value<int>() throw
// type_error.302; that exception becomes dispatch_failed and the Qt
// caller gets an EMPTY list — one malformed entry dropping every token.
// Validate and skip just this entry (a wrong decimals would misrender
// amounts, so fail closed) instead.
const auto decimals = entry.find("decimals");
if (decimals == entry.end() || !decimals->is_number_unsigned()) continue;
json token;
token["symbol"] = jStr(entry, "symbol");
token["name"] = jStr(entry, "name");
token["definitionId"] = definition_id;
token["holding"] = holding;
token["decimals"] = decimals->get<int>();
out.push_back(token);
}
return out;
}
LogosList AmmModuleImpl::tokenHoldings(bool wallet_open) {
const std::string amm_program_id = ammProgramId();
if (amm_program_id.empty())
-6
View File
@@ -236,12 +236,6 @@ public:
/// labels and decides selectability.
LogosList feeTiers();
/// Reads the token list config at TOKENS_CONFIG (a JSON array of
/// { symbol, name, definitionId, holding, decimals }) and returns it,
/// normalizing definitionId/holding to lowercase hex. Empty list if
/// TOKENS_CONFIG is unset / unreadable / not a JSON array.
LogosList tokenList();
/// Resolves an app-provided set of token ids into liquidity selector rows.
/// `request` carries `{ tokenIds: [<definition id>, …] }` (base58 or hex,
/// normalized to hex here) — the app owns the set: its configured tokens plus any