diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index 29b2a3c..fcc5106 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -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()) diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index a0105da..617c381 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -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. diff --git a/apps/amm/src/AmmUiBackend.rep b/apps/amm/src/AmmUiBackend.rep index 4e5b597..b90ea2d 100644 --- a/apps/amm/src/AmmUiBackend.rep +++ b/apps/amm/src/AmmUiBackend.rep @@ -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` diff --git a/modules/amm/README.md b/modules/amm/README.md index e44b365..ff67c63 100644 --- a/modules/amm/README.md +++ b/modules/amm/README.md @@ -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 `. +**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: diff --git a/modules/amm/src/amm_module_impl.cpp b/modules/amm/src/amm_module_impl.cpp index 2ec9a6e..5dda201 100644 --- a/modules/amm/src/amm_module_impl.cpp +++ b/modules/amm/src/amm_module_impl.cpp @@ -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(file)), - std::istreambuf_iterator()); - - 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() 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(); - out.push_back(token); - } - return out; -} - LogosList AmmModuleImpl::tokenHoldings(bool wallet_open) { const std::string amm_program_id = ammProgramId(); if (amm_program_id.empty()) diff --git a/modules/amm/src/amm_module_impl.h b/modules/amm/src/amm_module_impl.h index 62c1fee..bd4eec3 100644 --- a/modules/amm/src/amm_module_impl.h +++ b/modules/amm/src/amm_module_impl.h @@ -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: [, …] }` (base58 or hex, /// normalized to hex here) — the app owns the set: its configured tokens plus any