mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
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:
@@ -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())
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user