From b14788200dcb13896a2274ea79f581cc0e4927a7 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:56:42 +0200 Subject: [PATCH 1/2] feat(amm): move all AMM logic into the amm_module core module; flip UI to consume it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce modules/amm — the AMM business logic as a universal core Logos module, consumed identically by the QML UI (via modules().amm_module) and headlessly (logoscore call amm_module ...). The module is a thin transport adapter: the domain math lives in the Rust amm_client crate (the transport-independent JSON FFI), and the module sequences those pure ops with chain I/O delegated to the logos_execution_zone wallet module. It reaches the same shared wallet instance the UI opened (Basecamp loads core modules as singletons; standalone the LogosAPI client cache dedups the connection), so it never opens a second wallet. The module owns the full AMM surface — not just swaps: - resolvePool / swapExactInput / tokenList (the swap path) - newPositionContext / quoteNewPosition / submitNewPosition (add-liquidity) apps/amm: delete the app-side orchestration (SwapRuntime, NewPositionRuntime, AmmClient/BundledAmmClient) and the amm_client link. AmmUiBackend now owns only wallet-session lifecycle and forwards every AMM slot to modules().amm_module. The one wallet-keyset mutation add-liquidity needs — creating a fresh LP holding — stays in the backend (via its wallet provider, keeping the account model and on-disk storage coherent): the module returns "requires_fresh_lp" without submitting, the backend creates the account and resubmits with its id. flake.nix / CMakeLists / metadata: the module links the amm_client crate; the UI links no external lib and depends on amm_module (injected into the UI builder's flakeInputs so the dependency resolves). - amounts/deadline declared nlohmann::json so the generated dispatch accepts a JSON number (bare small ints on the CLI) or a string (exact u128 from the UI, or a quote-wrapped big value on the CLI); JSON floats are rejected rather than submit a silently-rounded amount. - AMM_DEBUG-gated tracing for the swap path. - Drop tests/cpp/NewPositionRuntimeTest.cpp with the class it covered (module-level tests to follow). - modules/amm/README.md: architecture, headless prerequisites, logoscore recipe. --- apps/amm/CMakeLists.txt | 30 +- apps/amm/README.md | 13 +- apps/amm/metadata.json | 8 +- apps/amm/src/ActiveNetwork.h | 17 - apps/amm/src/AmmClient.cpp | 91 --- apps/amm/src/AmmClient.h | 38 - apps/amm/src/AmmUiBackend.cpp | 278 ++----- apps/amm/src/AmmUiBackend.h | 63 +- apps/amm/src/NewPositionRuntime.cpp | 390 --------- apps/amm/src/NewPositionRuntime.h | 42 - apps/amm/src/SwapRuntime.cpp | 145 ---- apps/amm/src/SwapRuntime.h | 46 -- apps/amm/tests/cpp/NewPositionRuntimeTest.cpp | 230 ------ flake.nix | 44 +- modules/amm/CMakeLists.txt | 31 + modules/amm/README.md | 260 ++++++ modules/amm/flake.nix | 29 + modules/amm/metadata.json | 26 + modules/amm/src/amm_module_impl.cpp | 742 ++++++++++++++++++ modules/amm/src/amm_module_impl.h | 146 ++++ 20 files changed, 1374 insertions(+), 1295 deletions(-) delete mode 100644 apps/amm/src/ActiveNetwork.h delete mode 100644 apps/amm/src/AmmClient.cpp delete mode 100644 apps/amm/src/AmmClient.h delete mode 100644 apps/amm/src/NewPositionRuntime.cpp delete mode 100644 apps/amm/src/NewPositionRuntime.h delete mode 100644 apps/amm/src/SwapRuntime.cpp delete mode 100644 apps/amm/src/SwapRuntime.h delete mode 100644 apps/amm/tests/cpp/NewPositionRuntimeTest.cpp create mode 100644 modules/amm/CMakeLists.txt create mode 100644 modules/amm/README.md create mode 100644 modules/amm/flake.nix create mode 100644 modules/amm/metadata.json create mode 100644 modules/amm/src/amm_module_impl.cpp create mode 100644 modules/amm/src/amm_module_impl.h diff --git a/apps/amm/CMakeLists.txt b/apps/amm/CMakeLists.txt index 7f0121c1..0d075360 100644 --- a/apps/amm/CMakeLists.txt +++ b/apps/amm/CMakeLists.txt @@ -4,9 +4,6 @@ project(AmmUiPlugin LANGUAGES CXX) find_package(Qt6 6.8 REQUIRED COMPONENTS Core Gui Network Qml Quick QuickControls2) qt_standard_project_setup(REQUIRES 6.8) -find_package(PkgConfig REQUIRED) -pkg_check_modules(BASE58 REQUIRED IMPORTED_TARGET libbase58) - include(CTest) if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) @@ -27,6 +24,9 @@ add_subdirectory("${LOGOS_WALLET_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/share # ui_qml module with a hand-written C++ backend (QtRO .rep view contract + # generated *SimpleSource/*ViewPluginBase). Mirrors the LEZ wallet UI module. +# The AMM business logic lives in the amm_module core module (declared as a +# dependency in metadata.json, reached via modules().amm_module in the backend), +# so the UI links no amm_client library of its own. logos_module( NAME amm_ui REP_FILE src/AmmUiBackend.rep @@ -36,34 +36,10 @@ logos_module( src/AmmUiPlugin.cpp src/AmmUiBackend.h src/AmmUiBackend.cpp - src/ActiveNetwork.h - src/AmmClient.h - src/AmmClient.cpp - src/NewPositionRuntime.h - src/NewPositionRuntime.cpp - src/SwapRuntime.h - src/SwapRuntime.cpp FIND_PACKAGES Qt6Gui LINK_LIBRARIES Qt6::Gui - PkgConfig::BASE58 LINK_TARGETS logos_wallet_access - EXTERNAL_LIBS - amm_client ) - -if(BUILD_TESTING) - add_executable(amm_new_position_runtime_test - tests/cpp/NewPositionRuntimeTest.cpp - src/NewPositionRuntime.cpp - ) - target_include_directories(amm_new_position_runtime_test PRIVATE src) - target_link_libraries(amm_new_position_runtime_test PRIVATE - Qt6::Core - PkgConfig::BASE58 - logos_wallet_access - ) - add_test(NAME amm_new_position_runtime COMMAND amm_new_position_runtime_test) -endif() diff --git a/apps/amm/README.md b/apps/amm/README.md index b33841a0..5a4f4eee 100644 --- a/apps/amm/README.md +++ b/apps/amm/README.md @@ -52,9 +52,9 @@ This makes `lgpm` available as a global command. ## Running the UI standalone -The app is built from the **repository-root** flake (which also provides the -`amm_client_ffi` library it links). From the repo root, launch it with its named -attribute: +The app is built from the **repository-root** flake (which also builds the +`amm_module` core module the UI delegates its AMM logic to). From the repo root, +launch it with its named attribute: ```bash nix run .#amm-ui @@ -62,9 +62,10 @@ nix run .#amm-ui This builds and runs the application in development mode. The Logos bridge is unavailable in standalone mode, but the UI layout and mock data are fully functional. -Build just the FFI crate with `nix build .#amm_client_ffi`. (Each UI is exposed -under its own name, so future apps are `nix run .#` — there is no bare -`nix run .` default.) +Build just the AMM core module with `nix build .#amm-module`, or its underlying +client crate with `nix build .#amm_client`. (Each UI is exposed under its own +name, so future apps are `nix run .#` — there is no bare `nix run .` +default.) ## Running inside Logos Basecamp diff --git a/apps/amm/metadata.json b/apps/amm/metadata.json index 7b123c9c..5525cefb 100644 --- a/apps/amm/metadata.json +++ b/apps/amm/metadata.json @@ -7,20 +7,18 @@ "main": "amm_ui_plugin", "view": "qml/Main.qml", "icon": "icons/amm.png", - "dependencies": ["logos_execution_zone"], + "dependencies": ["logos_execution_zone", "amm_module"], "nix": { "packages": { "build": ["pkg-config"], "runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp", "libbase58"] }, - "external_libraries": [ - { "name": "amm_client" } - ], + "external_libraries": [], "cmake": { "find_packages": [], "extra_sources": [], - "extra_include_dirs": ["lib"], + "extra_include_dirs": [], "extra_link_libraries": [] } } diff --git a/apps/amm/src/ActiveNetwork.h b/apps/amm/src/ActiveNetwork.h deleted file mode 100644 index 771442b9..00000000 --- a/apps/amm/src/ActiveNetwork.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include -#include - -// Network context handed to the new-position flow. The AMM deployment identity -// (ammProgramId, from $AMM_PROGRAM_BIN) and the configured token set (tokenIds, -// from $TOKENS_CONFIG) are the same sources the Swap view uses; there is no -// separate network config file or channel-identity probe. `fingerprint` binds a -// quote to the deployment so a quote can't be replayed against a different one. -struct ActiveNetworkSnapshot { - QString id; - QString status; - QString fingerprint; - QString ammProgramId; - QStringList tokenIds; -}; diff --git a/apps/amm/src/AmmClient.cpp b/apps/amm/src/AmmClient.cpp deleted file mode 100644 index c78ce181..00000000 --- a/apps/amm/src/AmmClient.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "AmmClient.h" - -#include -#include -#include - -#include "amm_client.h" - -namespace { - using Operation = char* (*)(const char*); - - AmmClientResult call(Operation operation, const QJsonObject& request) - { - const QByteArray payload = QJsonDocument(request).toJson(QJsonDocument::Compact); - char* raw = operation(payload.constData()); - if (!raw) { - qWarning() << "AmmClient: bundled client returned a null response"; - return {}; - } - const QByteArray response(raw); - amm_free(raw); - - QJsonParseError parseError; - const QJsonDocument document = QJsonDocument::fromJson(response, &parseError); - if (parseError.error != QJsonParseError::NoError || !document.isObject()) { - qWarning() << "AmmClient: bundled client returned invalid JSON"; - return {}; - } - const QJsonObject envelope = document.object(); - if (!envelope.value(QStringLiteral("ok")).toBool()) { - qWarning() << "AmmClient: bundled client failure:" - << envelope.value(QStringLiteral("error")).toString(); - return {}; - } - if (!envelope.value(QStringLiteral("value")).isObject()) { - qWarning() << "AmmClient: bundled client value is not an object"; - return {}; - } - return { true, envelope.value(QStringLiteral("value")).toObject() }; - } -} - -AmmClientResult BundledAmmClient::configId(const QJsonObject& request) const -{ - return call(amm_config_id, request); -} - -AmmClientResult BundledAmmClient::tokenIds(const QJsonObject& request) const -{ - return call(amm_token_ids, request); -} - -AmmClientResult BundledAmmClient::pairIds(const QJsonObject& request) const -{ - return call(amm_pair_ids, request); -} - -AmmClientResult BundledAmmClient::context(const QJsonObject& request) const -{ - return call(amm_context, request); -} - -AmmClientResult BundledAmmClient::quote(const QJsonObject& request) const -{ - return call(amm_quote, request); -} - -AmmClientResult BundledAmmClient::plan(const QJsonObject& request) const -{ - return call(amm_plan, request); -} - -AmmClientResult BundledAmmClient::swapPair(const QJsonObject& request) const -{ - return call(amm_swap_pair, request); -} - -AmmClientResult BundledAmmClient::resolvePool(const QJsonObject& request) const -{ - return call(amm_resolve_pool, request); -} - -AmmClientResult BundledAmmClient::swapPlan(const QJsonObject& request) const -{ - return call(amm_swap_plan, request); -} - -AmmClientResult BundledAmmClient::programId(const QJsonObject& request) const -{ - return call(amm_program_id, request); -} diff --git a/apps/amm/src/AmmClient.h b/apps/amm/src/AmmClient.h deleted file mode 100644 index b78cc9b4..00000000 --- a/apps/amm/src/AmmClient.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include - -struct AmmClientResult { - bool ok = false; - QJsonObject value; -}; - -class AmmClient { -public: - virtual ~AmmClient() = default; - - virtual AmmClientResult configId(const QJsonObject& request) const = 0; - virtual AmmClientResult tokenIds(const QJsonObject& request) const = 0; - virtual AmmClientResult pairIds(const QJsonObject& request) const = 0; - virtual AmmClientResult context(const QJsonObject& request) const = 0; - virtual AmmClientResult quote(const QJsonObject& request) const = 0; - virtual AmmClientResult plan(const QJsonObject& request) const = 0; - virtual AmmClientResult swapPair(const QJsonObject& request) const = 0; - virtual AmmClientResult resolvePool(const QJsonObject& request) const = 0; - virtual AmmClientResult swapPlan(const QJsonObject& request) const = 0; - virtual AmmClientResult programId(const QJsonObject& request) const = 0; -}; - -class BundledAmmClient final : public AmmClient { -public: - AmmClientResult configId(const QJsonObject& request) const override; - AmmClientResult tokenIds(const QJsonObject& request) const override; - AmmClientResult pairIds(const QJsonObject& request) const override; - AmmClientResult context(const QJsonObject& request) const override; - AmmClientResult quote(const QJsonObject& request) const override; - AmmClientResult plan(const QJsonObject& request) const override; - AmmClientResult swapPair(const QJsonObject& request) const override; - AmmClientResult resolvePool(const QJsonObject& request) const override; - AmmClientResult swapPlan(const QJsonObject& request) const override; - AmmClientResult programId(const QJsonObject& request) const override; -}; diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index 60a3836d..312949b5 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -1,46 +1,50 @@ #include "AmmUiBackend.h" -#include -#include - -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include "AmmClient.h" #include "LogosWalletProvider.h" -#include "NewPositionRuntime.h" -#include "SwapRuntime.h" #include "WalletController.h" #include "logos_api.h" #include "logos_sdk.h" namespace { - // Absolute path to the deployed AMM program's RISC Zero program binary - // (amm.bin — the `ProgramBinary` `.bin` from the docker guest build, decoded - // on the Rust side via `ProgramBinary::decode`; NOT a raw ELF — pointing at - // the raw guest ELF yields a different/failed program id). The app can't - // safely embed/derive this itself: the wallet module's bundled AMM program - // may differ from whatever is actually deployed on the target sequencer, and - // the binary's bytes are what determine its program id (and therefore every - // PDA derived from it). See apps/amm/README.md. - const char AMM_PROGRAM_BIN_ENV[] = "AMM_PROGRAM_BIN"; + const char NEW_POSITION_SCHEMA[] = "new-position.v1"; + + // The new-position context placeholder published before the module + // connection is up (matches the module's "loading" contextState). + QVariantMap loadingContext() + { + return QVariantMap { + { QStringLiteral("schema"), QString::fromLatin1(NEW_POSITION_SCHEMA) }, + { QStringLiteral("status"), QStringLiteral("loading") }, + { QStringLiteral("networkId"), QStringLiteral("lez") }, + { QStringLiteral("networkFingerprint"), QString() }, + { QStringLiteral("tokens"), QVariantList() }, + { QStringLiteral("feeTiers"), QVariantList() }, + { QStringLiteral("warnings"), QVariantList() }, + }; + } - // Absolute path to the JSON token-list config consumed by tokenList() - // (see apps/amm/README.md). Config-driven so the Swap view's token picker - // doesn't need a hardcoded/dummy token list. - const char TOKENS_CONFIG_ENV[] = "TOKENS_CONFIG"; + // A new-position.v1 error envelope (matches the module's publicError), for + // the backend-side failure paths (e.g. LP-account creation failing). + QVariantMap newPositionError(const QString& code) + { + return QVariantMap { + { QStringLiteral("schema"), QString::fromLatin1(NEW_POSITION_SCHEMA) }, + { QStringLiteral("status"), QStringLiteral("error") }, + { QStringLiteral("canSubmit"), false }, + { QStringLiteral("code"), code }, + { QStringLiteral("errors"), QVariantList { QVariantMap { + { QStringLiteral("code"), code }, + { QStringLiteral("recoverable"), true }, + { QStringLiteral("blockingFields"), QVariantList() }, + { QStringLiteral("details"), QVariantMap() }, + } } }, + { QStringLiteral("warnings"), QVariantList() }, + { QStringLiteral("accountPreview"), QVariantList() }, + }; + } } AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent) @@ -49,17 +53,14 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent) m_logos(std::make_unique(m_logosAPI)), m_wallet(std::make_unique(m_logosAPI)), m_walletController(std::make_unique( - *m_wallet, QStringLiteral("AmmUI"))), - m_ammClient(std::make_unique()), - m_newPosition(std::make_unique(m_wallet.get(), m_ammClient.get())), - m_swap(std::make_unique(m_wallet.get(), m_ammClient.get())) + *m_wallet, QStringLiteral("AmmUI"))) { setWalletStateReady(false); - setNewPositionContext(m_newPosition->context( - QVariantMap(), networkSnapshot(), false, false)); connect(m_walletController.get(), &WalletController::stateChanged, this, &AmmUiBackend::syncWalletState); + // Publishes an initial "loading" context (walletStateReady is still false, + // so it does not yet reach the module). syncWalletState(); m_walletController->start(); QTimer::singleShot(0, this, [this]() { @@ -107,7 +108,6 @@ void AmmUiBackend::disconnectWallet() { m_walletController->disconnect(); setWalletStateReady(true); - m_newPosition->clearWalletAccounts(); refreshNewPositionContext(QVariantMap()); } @@ -147,25 +147,45 @@ void AmmUiBackend::refreshNewPositionContext(QVariantMap request) else { request = m_newPositionHints; } - setNewPositionContext(m_newPosition->context( - request, networkSnapshot(), isWalletOpen(), refreshWalletAccounts)); + if (!walletStateReady()) { + setNewPositionContext(loadingContext()); + return; + } + setNewPositionContext(m_logos->amm_module.newPositionContext( + request, isWalletOpen(), refreshWalletAccounts)); } QVariantMap AmmUiBackend::quoteNewPosition(QVariantMap request) { - return m_newPosition->quote(request, networkSnapshot(), isWalletOpen()); + return m_logos->amm_module.quoteNewPosition(request, isWalletOpen()); } QVariantMap AmmUiBackend::submitNewPosition(QVariantMap request, QString quoteHash) { - return m_newPosition->submit( - request, quoteHash, networkSnapshot(), isWalletOpen()); + // First attempt with no LP account. If the module needs a fresh LP holding + // it returns "requires_fresh_lp" without submitting; we own wallet-keyset + // mutation, so create the account here (keeping the account model + on-disk + // storage coherent) and resubmit with its id. + QVariantMap result = m_logos->amm_module.submitNewPosition( + request, quoteHash, isWalletOpen(), QString()); + + if (result.value(QStringLiteral("status")).toString() + == QStringLiteral("requires_fresh_lp")) { + const QString lpId = m_walletController->createAccount(true); + if (lpId.isEmpty()) + return newPositionError(QStringLiteral("wallet_submission_failed")); + result = m_logos->amm_module.submitNewPosition( + request, quoteHash, isWalletOpen(), lpId); + } + + if (result.value(QStringLiteral("status")).toString() == QStringLiteral("submitted")) + refreshBalances(); + return result; } void AmmUiBackend::syncWalletState() { const WalletUiState& state = m_walletController->state(); - const bool walletWasOpen = isWalletOpen(); setIsWalletOpen(state.isWalletOpen); setWalletExists(state.walletExists); @@ -177,129 +197,31 @@ void AmmUiBackend::syncWalletState() setSequencerAddr(state.sequencerAddress); setSequencerReachable(state.sequencerReachable); - if (walletWasOpen && !state.isWalletOpen) - m_newPosition->clearWalletAccounts(); - publishNetworkContext(); } void AmmUiBackend::publishNetworkContext() { - setNewPositionContext(m_newPosition->context( - m_newPositionHints, networkSnapshot(), isWalletOpen(), false)); -} - -QString AmmUiBackend::ammProgramIdHex() -{ - const QByteArray elf = loadAmmElf(); - if (elf.isEmpty()) - return QString(); - // Hand the deployed program binary to the amm_client program_id op, which - // decodes it and computes the Image ID — 64-char lowercase hex, little-endian - // per u32 word (matches `spel program-id` and the on-chain *_program_id fields). - const AmmClientResult result = m_ammClient->programId( - QJsonObject { { QStringLiteral("elf"), QString::fromLatin1(elf.toHex()) } }); - if (!result.ok) { - qWarning() << "AmmUiBackend::ammProgramIdHex: amm_program_id failed"; - return QString(); - } - return result.value.value(QStringLiteral("programId")).toString(); -} - -ActiveNetworkSnapshot AmmUiBackend::networkSnapshot() -{ - ActiveNetworkSnapshot snapshot; - snapshot.id = QStringLiteral("lez"); - // Defer program/token resolution (which reaches the module) until wallet - // state is resolved; the constructor publishes an initial context before - // the module is up, and syncWalletState() republishes once it is. if (!walletStateReady()) { - snapshot.status = QStringLiteral("loading"); - return snapshot; - } - // Resolve the AMM deployment id ($AMM_PROGRAM_BIN) and configured token set - // ($TOKENS_CONFIG) ONCE and cache — they're fixed for the process lifetime. - // networkSnapshot() runs on the quote hot path and from inside runtime reply - // callbacks, and tokenList() makes remote base58 conversions; recomputing each - // call reenters the module connection and hangs the reply. - if (!m_networkResolved) { - m_ammProgramIdCache = ammProgramIdHex(); - m_tokenIdsCache.clear(); - // Configured token set = the TOKENS_CONFIG definition ids, the same source - // the Swap view's token picker uses (tokenList normalizes them to hex). - const QVariantList tokens = tokenList(); - for (const QVariant& entry : tokens) { - const QString id = entry.toMap().value(QStringLiteral("definitionId")).toString(); - if (!id.isEmpty()) - m_tokenIdsCache.append(id); - } - m_networkResolved = true; + setNewPositionContext(loadingContext()); + return; } - snapshot.ammProgramId = m_ammProgramIdCache; - // Bind a quote to this AMM deployment: the program id changes per deployment, - // so it doubles as the network fingerprint (a quote can't be replayed against - // a different program). Empty when AMM_PROGRAM_BIN is unset — status gates it. - snapshot.fingerprint = m_ammProgramIdCache; - snapshot.tokenIds = m_tokenIdsCache; - snapshot.status = m_ammProgramIdCache.isEmpty() - ? QStringLiteral("config_missing") - : QStringLiteral("ready"); - return snapshot; -} - -QString AmmUiBackend::normalizeAccountId(const QString& id) -{ - const QString t = id.trimmed(); - // Already 64 hex chars? - if (t.size() == 64) { - bool allHex = true; - for (const QChar c : t) { - if (!std::isxdigit(static_cast(c.toLatin1()))) { - allHex = false; - break; - } - } - if (allHex) - return t.toLower(); - } - // Try base58 -> hex via the wallet module. - const QString hex = m_logos->logos_execution_zone.account_id_from_base58(t); - return hex.toLower(); // account_id_from_base58 returns "" on failure -} - -QByteArray AmmUiBackend::loadAmmElf() -{ - const QByteArray binPath = qgetenv(AMM_PROGRAM_BIN_ENV); - if (binPath.isEmpty()) { - qWarning() << "AmmUiBackend::loadAmmElf: AMM_PROGRAM_BIN not set"; - return QByteArray(); - } - QFile elfFile(QString::fromLocal8Bit(binPath)); - if (!elfFile.open(QIODevice::ReadOnly)) { - qWarning() << "AmmUiBackend::loadAmmElf: cannot read AMM_PROGRAM_BIN at" << elfFile.fileName(); - return QByteArray(); - } - const QByteArray elf = elfFile.readAll(); - elfFile.close(); - if (elf.isEmpty()) { - qWarning() << "AmmUiBackend::loadAmmElf: AMM_PROGRAM_BIN is empty"; - return QByteArray(); - } - return elf; + setNewPositionContext(m_logos->amm_module.newPositionContext( + m_newPositionHints, isWalletOpen(), false)); } QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex) { - return m_swap->resolvePool(defAHex, defBHex, networkSnapshot()); + return m_logos->amm_module.resolvePool(defAHex, defBHex); } QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex, QString userOutputHoldingHex, QString amountInDecimal, QString minOutDecimal, QString deadlineDecimal) { - const QString txHash = m_swap->swap(defAHex, defBHex, userInputHoldingHex, userOutputHoldingHex, - amountInDecimal, minOutDecimal, deadlineDecimal, - networkSnapshot(), isWalletOpen()); + const QString txHash = m_logos->amm_module.swapExactInput( + defAHex, defBHex, userInputHoldingHex, userOutputHoldingHex, + amountInDecimal, minOutDecimal, deadlineDecimal); if (!txHash.isEmpty()) refreshBalances(); return txHash; @@ -307,59 +229,5 @@ QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString u QVariantList AmmUiBackend::tokenList() { - QVariantList out; - - const QByteArray path = qgetenv(TOKENS_CONFIG_ENV); - if (path.isEmpty()) { - qWarning() << "AmmUiBackend::tokenList: TOKENS_CONFIG not set"; - return out; - } - - QFile file(QString::fromLocal8Bit(path)); - if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "AmmUiBackend::tokenList: cannot read TOKENS_CONFIG at" << file.fileName(); - return out; - } - const QByteArray json = file.readAll(); - file.close(); - - QJsonParseError parseError{}; - const QJsonDocument doc = QJsonDocument::fromJson(json, &parseError); - if (parseError.error != QJsonParseError::NoError || !doc.isArray()) { - qWarning() << "AmmUiBackend::tokenList: TOKENS_CONFIG at" << file.fileName() - << "is not a valid JSON array:" << parseError.errorString(); - return out; - } - - const QJsonArray arr = doc.array(); - for (const QJsonValue& entry : arr) { - if (!entry.isObject()) { - qWarning() << "AmmUiBackend::tokenList: skipping non-object entry in TOKENS_CONFIG"; - continue; - } - const QJsonObject obj = entry.toObject(); - const QString symbol = obj.value(QStringLiteral("symbol")).toString(); - - // TOKENS_CONFIG entries may give definitionId/holding as hex or - // base58 (the wallet/runbook display base58) — normalize both to - // lowercase hex here so every downstream consumer (resolvePool, - // swapExactInput, and the QML's hex comparisons) can assume hex. - const QString definitionId = - normalizeAccountId(obj.value(QStringLiteral("definitionId")).toString()); - const QString holding = normalizeAccountId(obj.value(QStringLiteral("holding")).toString()); - if (definitionId.isEmpty() || holding.isEmpty()) { - qWarning() << "AmmUiBackend::tokenList: skipping token" << symbol - << "— cannot normalize definitionId/holding to hex (not valid hex or base58)"; - continue; - } - - QVariantMap token; - token[QStringLiteral("symbol")] = symbol; - token[QStringLiteral("name")] = obj.value(QStringLiteral("name")).toString(); - token[QStringLiteral("definitionId")] = definitionId; - token[QStringLiteral("holding")] = holding; - token[QStringLiteral("decimals")] = obj.value(QStringLiteral("decimals")).toInt(); - out.append(token); - } - return out; + return m_logos->amm_module.tokenList(); } diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index 005d4726..f90d4ef1 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -12,20 +12,23 @@ #include "rep_AmmUiBackend_source.h" -#include "ActiveNetwork.h" #include "WalletAccountModel.h" class LogosAPI; struct LogosModules; -class AmmClient; class LogosWalletProvider; -class NewPositionRuntime; -class SwapRuntime; class WalletController; // Source-side implementation of the AmmUiBackend .rep interface. // Inheriting from AmmUiBackendSimpleSource gives us the generated PROPs and // SLOTs from AmmUiBackend.rep — all the simple ones flow over QtRO. +// +// The AMM business logic (pool resolution, swaps, add-liquidity) lives in the +// amm_module core Logos module; this backend owns only wallet-session lifecycle +// and forwards every AMM slot to modules().amm_module. The one exception is +// creating a fresh LP holding for add-liquidity: that mutates the wallet keyset, +// so it stays here (via the wallet provider, which keeps the account model and +// on-disk storage coherent) and its id is handed to the module's submit. class AmmUiBackend : public AmmUiBackendSimpleSource { Q_OBJECT Q_PROPERTY(WalletAccountModel* accountModel READ accountModel CONSTANT) @@ -53,61 +56,35 @@ public slots: bool openExisting() override; void disconnectWallet() override; - // AMM + // AMM — all forwarded to the amm_module core module. QVariantMap resolvePool(QString defAHex, QString defBHex) override; QString swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex, QString userOutputHoldingHex, QString amountInDecimal, QString minOutDecimal, QString deadlineDecimal) override; - // Reads the token list from TOKENS_CONFIG (see AmmUiBackend.cpp) so the - // Swap UI's token picker is config-driven instead of hardcoded. + // Reads the token list from TOKENS_CONFIG (via the module) so the Swap UI's + // token picker is config-driven instead of hardcoded. QVariantList tokenList() override; private: void syncWalletState(); + // Publishes the new-position context PROP: a local "loading" placeholder + // until wallet state (and thus the module connection) is ready, then the + // module's newPositionContext for the current hints. void publishNetworkContext(); - // Builds the new-position network context from the same sources the Swap - // view uses: ammProgramId from $AMM_PROGRAM_BIN, tokenIds from - // $TOKENS_CONFIG. status is "ready" once AMM_PROGRAM_BIN resolves, else - // "config_missing". There is no separate network config or channel probe. - ActiveNetworkSnapshot networkSnapshot(); - - // 64-char lowercase-hex AMM program id derived from $AMM_PROGRAM_BIN (empty - // if unset/unreadable); matches swapExactInput's program-id encoding. - QString ammProgramIdHex(); - - // Normalizes an account id given as either 64-char lowercase/uppercase hex - // or base58 to lowercase hex. Returns an empty QString if `id` is neither - // (or the base58 decode fails), so callers can detect and skip it. - QString normalizeAccountId(const QString& id); - - // Returns the deployed AMM program-binary bytes (a RISC Zero ProgramBinary - // .bin, not a raw ELF) from $AMM_PROGRAM_BIN, or an empty QByteArray (with a - // qWarning) if the env var is unset/unreadable/empty. - QByteArray loadAmmElf(); - LogosAPI* m_logosAPI; - // Direct module handle for the AMM/swap path (resolvePool/swapExactInput/ - // tokenList). The shared wallet provider exposes only wallet-level ops, not - // the raw account-id / get_account_public / send_generic_public_transaction - // calls the AMM path needs, so keep a thin LogosModules over the same - // LogosAPI as the wallet provider. + // Handle for the amm_module core module (resolvePool / swapExactInput / + // tokenList / new-position). The module wraps the amm_client 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. std::unique_ptr m_logos; std::unique_ptr m_wallet; std::unique_ptr m_walletController; - std::unique_ptr m_ammClient; - std::unique_ptr m_newPosition; - std::unique_ptr m_swap; + // Sticky new-position hints (recent/resolved token ids) so a bare + // republish (wallet-state change) keeps the user's last selection. QVariantMap m_newPositionHints; - - // Network context is derived from $AMM_PROGRAM_BIN + $TOKENS_CONFIG, which are - // fixed for the process lifetime — resolve them once and cache. networkSnapshot() - // runs on the hot path (every quote) and from inside runtime callbacks, and - // tokenList() makes remote base58 conversions, so it must not recompute each call. - bool m_networkResolved = false; - QString m_ammProgramIdCache; - QStringList m_tokenIdsCache; }; #endif // AMM_UI_BACKEND_H diff --git a/apps/amm/src/NewPositionRuntime.cpp b/apps/amm/src/NewPositionRuntime.cpp deleted file mode 100644 index 3f016ef6..00000000 --- a/apps/amm/src/NewPositionRuntime.cpp +++ /dev/null @@ -1,390 +0,0 @@ -#include "NewPositionRuntime.h" - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "AmmClient.h" -#include "WalletProvider.h" - -namespace { - const char SCHEMA[] = "new-position.v1"; - constexpr qsizetype HASH_BYTES = 32; - constexpr std::size_t BASE58_BUFFER_SIZE = 45; - - QString base58TransactionId(const QString& transactionHash) - { - const QByteArray hex = transactionHash.toLatin1(); - const QByteArray bytes = QByteArray::fromHex(hex); - if (bytes.size() != HASH_BYTES || bytes.toHex() != hex) - return {}; - - std::array encoded {}; - std::size_t size = encoded.size(); - if (!b58enc(encoded.data(), &size, bytes.constData(), - static_cast(bytes.size()))) { - return {}; - } - return QString::fromLatin1( - encoded.data(), static_cast(size - 1)); - } - - QJsonObject issue(const QString& code, - const QJsonArray& blockingFields = {}) - { - return { - { QStringLiteral("code"), code }, - { QStringLiteral("recoverable"), true }, - { QStringLiteral("blockingFields"), blockingFields }, - { QStringLiteral("details"), QJsonObject() }, - }; - } - - QJsonObject publicError(const QString& code, - const QJsonArray& blockingFields = {}, - const QJsonObject& details = {}) - { - QJsonObject error = issue(code, blockingFields); - error.insert(QStringLiteral("details"), details); - return { - { QStringLiteral("schema"), QString::fromLatin1(SCHEMA) }, - { QStringLiteral("status"), QStringLiteral("error") }, - { QStringLiteral("canSubmit"), false }, - { QStringLiteral("code"), code }, - { QStringLiteral("errors"), QJsonArray { error } }, - { QStringLiteral("warnings"), QJsonArray() }, - { QStringLiteral("accountPreview"), QJsonArray() }, - }; - } - - QJsonObject contextState(const QString& status, - const ActiveNetworkSnapshot& network, - const QString& code = {}) - { - QJsonObject state { - { QStringLiteral("schema"), QString::fromLatin1(SCHEMA) }, - { QStringLiteral("status"), status }, - { QStringLiteral("networkId"), network.id }, - { QStringLiteral("networkFingerprint"), network.fingerprint }, - { QStringLiteral("tokens"), QJsonArray() }, - { QStringLiteral("feeTiers"), QJsonArray() }, - { QStringLiteral("warnings"), QJsonArray() }, - }; - if (!code.isEmpty()) - state.insert(QStringLiteral("code"), code); - return state; - } - - QJsonArray variantStringArray(const QVariant& value) - { - QJsonArray result; - for (const QVariant& item : value.toList()) - result.append(item.toString()); - return result; - } - - QStringList jsonStringList(const QJsonArray& values) - { - QStringList result; - result.reserve(values.size()); - for (const QJsonValue& value : values) - result.append(value.toString()); - return result; - } - - QVector jsonBoolList(const QJsonArray& values) - { - QVector result; - result.reserve(values.size()); - for (const QJsonValue& value : values) - result.append(value.toBool()); - return result; - } - - QVector jsonUIntList(const QJsonArray& values) - { - QVector result; - result.reserve(values.size()); - for (const QJsonValue& value : values) - result.append(static_cast(value.toInteger())); - return result; - } - - QJsonObject accountReadJson(const WalletAccountRead& read) - { - QJsonObject result { - { QStringLiteral("id"), read.accountId }, - { QStringLiteral("status"), read.status }, - }; - if (read.ok()) { - result.insert(QStringLiteral("account"), QJsonObject { - { QStringLiteral("program_owner"), read.programOwner }, - { QStringLiteral("balance"), read.balanceHex }, - { QStringLiteral("nonce"), read.nonceHex }, - { QStringLiteral("data"), read.dataHex }, - }); - } - return result; - } - - QJsonArray accountReadsJson(const QVector& reads) - { - QJsonArray result; - for (const WalletAccountRead& read : reads) - result.append(accountReadJson(read)); - return result; - } -} - -NewPositionRuntime::NewPositionRuntime(WalletProvider* wallet, AmmClient* client) - : m_wallet(wallet), - m_client(client) -{ -} - -void NewPositionRuntime::clearWalletAccounts() -{ - m_wallet->clearSnapshot(); -} - -QJsonArray NewPositionRuntime::walletAccountReads(bool walletOpen, bool refresh) const -{ - if (!walletOpen) - return {}; - return accountReadsJson(m_wallet->snapshot(refresh).publicAccountReads); -} - -QJsonObject NewPositionRuntime::buildQuoteInput(const QVariantMap& request, - const ActiveNetworkSnapshot& network, - bool walletOpen, - bool freshWalletAccounts, - QJsonObject* error) const -{ - if (network.status != QStringLiteral("ready")) { - *error = publicError(network.status); - return {}; - } - const AmmClientResult configResult = m_client->configId( - QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } }); - if (!configResult.ok) { - *error = publicError(QStringLiteral("backend_error")); - return {}; - } - const QJsonObject configManifest = configResult.value; - const QJsonObject config = accountReadJson(m_wallet->readPublicAccount( - configManifest.value(QStringLiteral("configId")).toString())); - const QJsonObject requestObject = QJsonObject::fromVariantMap(request); - const AmmClientResult pairResult = m_client->pairIds( - QJsonObject { - { QStringLiteral("ammProgramId"), network.ammProgramId }, - { QStringLiteral("config"), config }, - { QStringLiteral("tokenAId"), requestObject.value(QStringLiteral("tokenAId")) }, - { QStringLiteral("tokenBId"), requestObject.value(QStringLiteral("tokenBId")) }, - }); - if (!pairResult.ok) { - *error = publicError(QStringLiteral("backend_error")); - return {}; - } - const QJsonObject pairManifest = pairResult.value; - if (pairManifest.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) { - *error = publicError(pairManifest.value(QStringLiteral("code")).toString()); - return {}; - } - - const QJsonArray walletAccounts = walletAccountReads(walletOpen, freshWalletAccounts); - const QJsonObject snapshot { - { QStringLiteral("config"), config }, - { QStringLiteral("tokenA"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("tokenAId")).toString())) }, - { QStringLiteral("tokenB"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("tokenBId")).toString())) }, - { QStringLiteral("pool"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("poolId")).toString())) }, - { QStringLiteral("vaultA"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("vaultAId")).toString())) }, - { QStringLiteral("vaultB"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("vaultBId")).toString())) }, - { QStringLiteral("lpDefinition"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("lpDefinitionId")).toString())) }, - { QStringLiteral("lpLockHolding"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("lpLockHoldingId")).toString())) }, - { QStringLiteral("currentTick"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("currentTickId")).toString())) }, - { QStringLiteral("clock"), accountReadJson(m_wallet->readPublicAccount(pairManifest.value(QStringLiteral("clockId")).toString())) }, - { QStringLiteral("walletAvailable"), walletOpen }, - { QStringLiteral("walletAccounts"), walletAccounts }, - }; - return { - { QStringLiteral("networkId"), network.id }, - { QStringLiteral("networkFingerprint"), network.fingerprint }, - { QStringLiteral("ammProgramId"), network.ammProgramId }, - { QStringLiteral("request"), requestObject }, - { QStringLiteral("snapshot"), snapshot }, - }; -} - -QVariantMap NewPositionRuntime::context(const QVariantMap& request, - const ActiveNetworkSnapshot& network, - bool walletOpen, - bool refreshWalletAccounts) -{ - if (network.status != QStringLiteral("ready")) - return contextState(network.status, network).toVariantMap(); - - const QJsonArray walletAccounts = walletAccountReads(walletOpen, refreshWalletAccounts); - - const QJsonObject hints = QJsonObject::fromVariantMap(request); - const AmmClientResult configResult = m_client->configId( - QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } }); - if (!configResult.ok) - return contextState( - QStringLiteral("error"), network, QStringLiteral("backend_error")).toVariantMap(); - - const QJsonObject configManifest = configResult.value; - const QJsonObject config = accountReadJson(m_wallet->readPublicAccount( - configManifest.value(QStringLiteral("configId")).toString())); - QJsonArray configured; - for (const QString& id : network.tokenIds) - configured.append(id); - const QJsonArray recent = variantStringArray(hints.value(QStringLiteral("recentTokenIds")).toVariant()); - const QJsonArray resolved = variantStringArray(hints.value(QStringLiteral("resolvedTokenIds")).toVariant()); - - const AmmClientResult tokenResult = m_client->tokenIds( - QJsonObject { - { QStringLiteral("ammProgramId"), network.ammProgramId }, - { QStringLiteral("config"), config }, - { QStringLiteral("walletAccounts"), walletAccounts }, - { QStringLiteral("configuredTokenIds"), configured }, - { QStringLiteral("recentTokenIds"), recent }, - { QStringLiteral("resolvedTokenIds"), resolved }, - }); - const QJsonObject tokenManifest = tokenResult.value; - if (!tokenResult.ok - || tokenManifest.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) { - const QString code = tokenResult.ok - ? tokenManifest.value(QStringLiteral("code")).toString() - : QStringLiteral("backend_error"); - return contextState( - QStringLiteral("error"), - network, - code.isEmpty() ? QStringLiteral("backend_error") : code).toVariantMap(); - } - - QJsonArray definitions; - for (const QJsonValue& id : tokenManifest.value(QStringLiteral("tokenIds")).toArray()) - definitions.append(accountReadJson(m_wallet->readPublicAccount(id.toString()))); - - const AmmClientResult contextResult = m_client->context( - QJsonObject { - { QStringLiteral("networkId"), network.id }, - { QStringLiteral("networkFingerprint"), network.fingerprint }, - { QStringLiteral("ammProgramId"), network.ammProgramId }, - { QStringLiteral("walletAvailable"), walletOpen }, - { QStringLiteral("config"), config }, - { QStringLiteral("walletAccounts"), walletAccounts }, - { QStringLiteral("tokenDefinitions"), definitions }, - { QStringLiteral("configuredTokenIds"), configured }, - { QStringLiteral("recentTokenIds"), recent }, - { QStringLiteral("resolvedTokenIds"), resolved }, - }); - return (contextResult.ok - ? contextResult.value - : contextState( - QStringLiteral("error"), network, QStringLiteral("backend_error"))).toVariantMap(); -} - -QVariantMap NewPositionRuntime::quote(const QVariantMap& request, - const ActiveNetworkSnapshot& network, - bool walletOpen) -{ - QJsonObject error; - const QJsonObject input = buildQuoteInput(request, network, walletOpen, false, &error); - if (!error.isEmpty()) - return error.toVariantMap(); - - const AmmClientResult result = m_client->quote(input); - return (result.ok ? result.value : publicError(QStringLiteral("backend_error"))).toVariantMap(); -} - -QVariantMap NewPositionRuntime::submit(const QVariantMap& request, - const QString& quoteHash, - const ActiveNetworkSnapshot& network, - bool walletOpen) -{ - if (m_submitInFlight) - return publicError(QStringLiteral("submit_in_progress")).toVariantMap(); - if (!walletOpen) - return publicError(QStringLiteral("wallet_unavailable")).toVariantMap(); - QScopedValueRollback submitGuard(m_submitInFlight, true); - - QJsonObject error; - const QJsonObject input = buildQuoteInput(request, network, walletOpen, true, &error); - if (!error.isEmpty()) - return error.toVariantMap(); - - const AmmClientResult quoteResult = m_client->quote(input); - if (!quoteResult.ok) - return publicError(QStringLiteral("backend_error")).toVariantMap(); - const QJsonObject quote = quoteResult.value; - if (quote.value(QStringLiteral("quoteHash")).toString() != quoteHash) { - QJsonObject result = publicError(QStringLiteral("quote_changed")); - result.insert(QStringLiteral("quote"), quote); - return result.toVariantMap(); - } - if (!quote.value(QStringLiteral("canSubmit")).toBool(false)) { - QJsonObject result = publicError(QStringLiteral("quote_not_submittable")); - result.insert(QStringLiteral("quote"), quote); - return result.toVariantMap(); - } - - QJsonValue freshLp; - if (quote.value(QStringLiteral("requiresFreshLp")).toBool(false)) { - const WalletAccountCreation creation = m_wallet->createAccount(true); - if (!creation.ok() || !creation.publicAccount.ok()) - return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap(); - freshLp = accountReadJson(creation.publicAccount); - } - - QJsonObject planInput = input; - planInput.insert(QStringLiteral("quoteHash"), quoteHash); - planInput.insert(QStringLiteral("nowMs"), QDateTime::currentMSecsSinceEpoch()); - if (!freshLp.isUndefined()) - planInput.insert(QStringLiteral("freshLp"), freshLp); - - const AmmClientResult planResult = m_client->plan(planInput); - if (!planResult.ok) - return publicError(QStringLiteral("backend_error")).toVariantMap(); - const QJsonObject plan = planResult.value; - if (plan.value(QStringLiteral("status")).toString() != QStringLiteral("ready")) { - const QString code = plan.value(QStringLiteral("code")).toString(); - return publicError(code.isEmpty() ? QStringLiteral("wallet_submission_failed") : code) - .toVariantMap(); - } - - const QStringList accountIds = jsonStringList(plan.value(QStringLiteral("accountIds")).toArray()); - const QVector signingRequirements = jsonBoolList(plan.value(QStringLiteral("signingRequirements")).toArray()); - const QVector instruction = jsonUIntList(plan.value(QStringLiteral("instruction")).toArray()); - const QString programId = plan.value(QStringLiteral("programId")).toString(); - bool deadlineValid = false; - const qulonglong deadline = plan.value(QStringLiteral("deadlineMs")).toString().toULongLong(&deadlineValid); - if (!deadlineValid - || static_cast(QDateTime::currentMSecsSinceEpoch()) >= deadline) { - return publicError(QStringLiteral("transaction_deadline_expired")).toVariantMap(); - } - const WalletSubmission submission = m_wallet->submitPublicTransaction({ - programId, - accountIds, - signingRequirements, - instruction, - }); - if (!submission.accepted()) - return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap(); - const QString transactionId = base58TransactionId(submission.nativeHash); - if (transactionId.isEmpty()) - return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap(); - - return QJsonObject { - { QStringLiteral("schema"), QString::fromLatin1(SCHEMA) }, - { QStringLiteral("status"), QStringLiteral("submitted") }, - { QStringLiteral("transactionId"), transactionId }, - { QStringLiteral("deadlineMs"), plan.value(QStringLiteral("deadlineMs")) }, - }.toVariantMap(); -} diff --git a/apps/amm/src/NewPositionRuntime.h b/apps/amm/src/NewPositionRuntime.h deleted file mode 100644 index 99e190e9..00000000 --- a/apps/amm/src/NewPositionRuntime.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "ActiveNetwork.h" - -class AmmClient; -class WalletProvider; - -class NewPositionRuntime { -public: - NewPositionRuntime(WalletProvider* wallet, AmmClient* client); - - void clearWalletAccounts(); - - QVariantMap context(const QVariantMap& request, - const ActiveNetworkSnapshot& network, - bool walletOpen, - bool refreshWalletAccounts); - QVariantMap quote(const QVariantMap& request, - const ActiveNetworkSnapshot& network, - bool walletOpen); - QVariantMap submit(const QVariantMap& request, - const QString& quoteHash, - const ActiveNetworkSnapshot& network, - bool walletOpen); - -private: - QJsonArray walletAccountReads(bool walletOpen, bool refresh) const; - QJsonObject buildQuoteInput(const QVariantMap& request, - const ActiveNetworkSnapshot& network, - bool walletOpen, - bool freshWalletAccounts, - QJsonObject* error) const; - - WalletProvider* m_wallet; - AmmClient* m_client; - bool m_submitInFlight = false; -}; diff --git a/apps/amm/src/SwapRuntime.cpp b/apps/amm/src/SwapRuntime.cpp deleted file mode 100644 index ea423d24..00000000 --- a/apps/amm/src/SwapRuntime.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include "SwapRuntime.h" - -#include -#include -#include - -#include "AmmClient.h" -#include "WalletProvider.h" - -namespace { - QJsonObject accountReadJson(const WalletAccountRead& read) - { - QJsonObject result { - { QStringLiteral("id"), read.accountId }, - { QStringLiteral("status"), read.status }, - }; - if (read.ok()) { - result.insert(QStringLiteral("account"), QJsonObject { - { QStringLiteral("program_owner"), read.programOwner }, - { QStringLiteral("balance"), read.balanceHex }, - { QStringLiteral("nonce"), read.nonceHex }, - { QStringLiteral("data"), read.dataHex }, - }); - } - return result; - } - - QStringList jsonStringList(const QJsonArray& values) - { - QStringList result; - result.reserve(values.size()); - for (const QJsonValue& value : values) - result.append(value.toString()); - return result; - } - - QVector jsonBoolList(const QJsonArray& values) - { - QVector result; - result.reserve(values.size()); - for (const QJsonValue& value : values) - result.append(value.toBool()); - return result; - } - - QVector jsonUIntList(const QJsonArray& values) - { - QVector result; - result.reserve(values.size()); - for (const QJsonValue& value : values) - result.append(static_cast(value.toInteger())); - return result; - } -} - -SwapRuntime::SwapRuntime(WalletProvider* wallet, AmmClient* client) - : m_wallet(wallet), - m_client(client) -{ -} - -QJsonObject SwapRuntime::readConfig(const ActiveNetworkSnapshot& network) const -{ - const AmmClientResult configResult = m_client->configId( - QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } }); - if (!configResult.ok) - return {}; - return accountReadJson(m_wallet->readPublicAccount( - configResult.value.value(QStringLiteral("configId")).toString())); -} - -QVariantMap SwapRuntime::resolvePool(const QString& tokenInId, - const QString& tokenOutId, - const ActiveNetworkSnapshot& network) -{ - const QVariantMap absent { { QStringLiteral("exists"), false } }; - if (network.status != QStringLiteral("ready")) - return absent; - - const QJsonObject config = readConfig(network); - if (config.isEmpty()) - return absent; - - const AmmClientResult pairResult = m_client->swapPair(QJsonObject { - { QStringLiteral("ammProgramId"), network.ammProgramId }, - { QStringLiteral("tokenInId"), tokenInId }, - { QStringLiteral("tokenOutId"), tokenOutId }, - { QStringLiteral("config"), config }, - }); - if (!pairResult.ok - || pairResult.value.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) - return absent; - - const QJsonObject pool = accountReadJson(m_wallet->readPublicAccount( - pairResult.value.value(QStringLiteral("poolId")).toString())); - const AmmClientResult resolveResult = - m_client->resolvePool(QJsonObject { { QStringLiteral("pool"), pool } }); - if (!resolveResult.ok) - return absent; - return resolveResult.value.toVariantMap(); -} - -QString SwapRuntime::swap(const QString& tokenInId, - const QString& tokenOutId, - const QString& userInputHoldingId, - const QString& userOutputHoldingId, - const QString& amountInDecimal, - const QString& minOutDecimal, - const QString& deadlineMs, - const ActiveNetworkSnapshot& network, - bool walletOpen) -{ - if (network.status != QStringLiteral("ready") || !walletOpen) - return {}; - - const QJsonObject config = readConfig(network); - if (config.isEmpty()) - return {}; - - const AmmClientResult planResult = m_client->swapPlan(QJsonObject { - { QStringLiteral("ammProgramId"), network.ammProgramId }, - { QStringLiteral("tokenInId"), tokenInId }, - { QStringLiteral("tokenOutId"), tokenOutId }, - { QStringLiteral("config"), config }, - { QStringLiteral("userInputHoldingId"), userInputHoldingId }, - { QStringLiteral("userOutputHoldingId"), userOutputHoldingId }, - { QStringLiteral("amountIn"), amountInDecimal }, - { QStringLiteral("minOut"), minOutDecimal }, - { QStringLiteral("deadlineMs"), deadlineMs }, - }); - if (!planResult.ok - || planResult.value.value(QStringLiteral("status")).toString() != QStringLiteral("ready")) - return {}; - - const QJsonObject plan = planResult.value; - const WalletSubmission submission = m_wallet->submitPublicTransaction({ - plan.value(QStringLiteral("programId")).toString(), - jsonStringList(plan.value(QStringLiteral("accountIds")).toArray()), - jsonBoolList(plan.value(QStringLiteral("signingRequirements")).toArray()), - jsonUIntList(plan.value(QStringLiteral("instruction")).toArray()), - }); - if (!submission.accepted()) - return {}; - return submission.nativeHash; -} diff --git a/apps/amm/src/SwapRuntime.h b/apps/amm/src/SwapRuntime.h deleted file mode 100644 index 4bf04f5a..00000000 --- a/apps/amm/src/SwapRuntime.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "ActiveNetwork.h" - -class AmmClient; -class WalletProvider; - -// Off-chain orchestration for the Swap view: reads accounts through the wallet, -// drives the amm_client swap ops, and submits the swap transaction. The AMM -// domain logic lives in the amm_client crate; this class only glues account -// reads and submission to it. Mirrors NewPositionRuntime. -class SwapRuntime { -public: - SwapRuntime(WalletProvider* wallet, AmmClient* client); - - // { exists, reserveA, reserveB, feeBps } for the (tokenIn, tokenOut) pool. - // reserveA/reserveB follow the pool's canonical def order (the caller maps - // them to sell/buy). exists=false when the pool is absent or has no liquidity. - QVariantMap resolvePool(const QString& tokenInId, - const QString& tokenOutId, - const ActiveNetworkSnapshot& network); - - // Builds and submits a SwapExactInput transaction; returns the native tx - // hash on success, an empty string on any failure. - QString swap(const QString& tokenInId, - const QString& tokenOutId, - const QString& userInputHoldingId, - const QString& userOutputHoldingId, - const QString& amountInDecimal, - const QString& minOutDecimal, - const QString& deadlineMs, - const ActiveNetworkSnapshot& network, - bool walletOpen); - -private: - // Derives the config account id (config_id) and reads it. Returns an empty - // object only when the config_id op itself fails. - QJsonObject readConfig(const ActiveNetworkSnapshot& network) const; - - WalletProvider* m_wallet; - AmmClient* m_client; -}; diff --git a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp deleted file mode 100644 index 38f174f9..00000000 --- a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp +++ /dev/null @@ -1,230 +0,0 @@ -#include "AmmClient.h" -#include "NewPositionRuntime.h" -#include "WalletProvider.h" - -#include - -namespace { - bool expect(bool condition, const char* message) - { - if (!condition) - qCritical("%s", message); - return condition; - } - - class FakeWallet final : public WalletProvider { - public: - WalletSession connect(const WalletPaths&) override - { - return {}; - } - - WalletCreation createWallet(const WalletPaths&, const QString&) override - { - return {}; - } - - WalletSnapshot snapshot(bool) override - { - return {}; - } - - void clearSnapshot() override {} - - WalletAccountCreation createAccount(bool isPublic) override - { - ++createdAccounts; - WalletAccountCreation creation; - creation.accountId = QStringLiteral("fresh-lp"); - if (isPublic) - creation.publicAccount = readPublicAccount(creation.accountId); - return creation; - } - - WalletAccountRead readPublicAccount(const QString& accountId) const override - { - WalletAccountRead read; - read.accountId = accountId; - read.status = QStringLiteral("ok"); - return read; - } - - WalletSubmission submitPublicTransaction(const WalletTransaction& transaction) override - { - ++submissions; - submitted = transaction; - return { WalletFailure::None, transactionHash }; - } - - void disconnect() override {} - - QString transactionHash = QStringLiteral( - "000102030405060708090a0b0c0d0e0f" - "101112131415161718191a1b1c1d1e1f"); - int createdAccounts = 0; - int submissions = 0; - WalletTransaction submitted; - }; - - class FakeAmmClient final : public AmmClient { - public: - AmmClientResult configId(const QJsonObject&) const override - { - return success({ { QStringLiteral("configId"), QStringLiteral("config") } }); - } - - AmmClientResult tokenIds(const QJsonObject&) const override - { - return success({ { QStringLiteral("status"), QStringLiteral("ok") } }); - } - - AmmClientResult pairIds(const QJsonObject&) const override - { - return success({ - { QStringLiteral("status"), QStringLiteral("ok") }, - { QStringLiteral("tokenAId"), QStringLiteral("token-a") }, - { QStringLiteral("tokenBId"), QStringLiteral("token-b") }, - { QStringLiteral("poolId"), QStringLiteral("pool") }, - { QStringLiteral("vaultAId"), QStringLiteral("vault-a") }, - { QStringLiteral("vaultBId"), QStringLiteral("vault-b") }, - { QStringLiteral("lpDefinitionId"), QStringLiteral("lp") }, - { QStringLiteral("lpLockHoldingId"), QStringLiteral("lp-lock") }, - { QStringLiteral("currentTickId"), QStringLiteral("tick") }, - { QStringLiteral("clockId"), QStringLiteral("clock") }, - }); - } - - AmmClientResult context(const QJsonObject&) const override - { - return success({}); - } - - AmmClientResult quote(const QJsonObject&) const override - { - return success({ - { QStringLiteral("schema"), QStringLiteral("new-position.v1") }, - { QStringLiteral("status"), QStringLiteral("ok") }, - { QStringLiteral("canSubmit"), true }, - { QStringLiteral("quoteHash"), quoteHash }, - { QStringLiteral("requiresFreshLp"), requiresFreshLp }, - }); - } - - AmmClientResult plan(const QJsonObject& request) const override - { - sawFreshLp = request.contains(QStringLiteral("freshLp")); - return success({ - { QStringLiteral("status"), QStringLiteral("ready") }, - { QStringLiteral("accountIds"), QJsonArray { QStringLiteral("account") } }, - { QStringLiteral("signingRequirements"), QJsonArray { true } }, - { QStringLiteral("instruction"), QJsonArray { 1 } }, - { QStringLiteral("programId"), QStringLiteral("program") }, - { QStringLiteral("deadlineMs"), - QString::number(QDateTime::currentMSecsSinceEpoch() + 60'000) }, - }); - } - - static AmmClientResult success(const QJsonObject& value) - { - return { true, value }; - } - - QString quoteHash = QStringLiteral("sha256:expected"); - bool requiresFreshLp = true; - mutable bool sawFreshLp = false; - }; - - ActiveNetworkSnapshot readyNetwork() - { - return { - QStringLiteral("testnet"), - QStringLiteral("ready"), - QStringLiteral("block10:identity"), - QStringLiteral("program"), - {}, - }; - } -} - -int main() -{ - const QVariantMap request { - { QStringLiteral("schema"), QStringLiteral("new-position.v1") }, - { QStringLiteral("tokenAId"), QStringLiteral("token-a") }, - { QStringLiteral("tokenBId"), QStringLiteral("token-b") }, - { QStringLiteral("feeBps"), 30 }, - }; - - FakeWallet wallet; - FakeAmmClient client; - NewPositionRuntime runtime(&wallet, &client); - const QVariantMap result = runtime.submit( - request, QStringLiteral("sha256:expected"), readyNetwork(), true); - if (!expect(result.value(QStringLiteral("status")).toString() - == QStringLiteral("submitted"), - "valid plan should submit")) - return 1; - if (!expect(result.value(QStringLiteral("transactionId")).toString() - == QStringLiteral("1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"), - "native hash should become the expected base58 transaction ID")) - return 1; - if (!expect(wallet.createdAccounts == 1 && client.sawFreshLp, - "fresh LP account should enter the plan")) - return 1; - if (!expect(wallet.submissions == 1 - && wallet.submitted.accountIds - == QStringList { QStringLiteral("account") } - && wallet.submitted.signingRequirements.size() == 1 - && wallet.submitted.signingRequirements.constFirst() - && wallet.submitted.instruction.size() == 1 - && wallet.submitted.instruction.constFirst() == 1 - && wallet.submitted.programId == QStringLiteral("program"), - "runtime should dispatch the unchanged plan once")) - return 1; - - FakeWallet orderedBytesWallet; - orderedBytesWallet.transactionHash = QStringLiteral( - "0102030405060708090a0b0c0d0e0f10" - "1112131415161718191a1b1c1d1e1f20"); - FakeAmmClient orderedBytesClient; - orderedBytesClient.requiresFreshLp = false; - NewPositionRuntime orderedBytesRuntime(&orderedBytesWallet, &orderedBytesClient); - const QVariantMap orderedBytes = orderedBytesRuntime.submit( - request, QStringLiteral("sha256:expected"), readyNetwork(), true); - if (!expect(orderedBytes.value(QStringLiteral("transactionId")).toString() - == QStringLiteral("4wBqpZM9xaSheZzJSMawUKKwhdpChKbZ5eu5ky4Vigw"), - "ordered native hash bytes should preserve byte order")) - return 1; - - FakeWallet invalidHashWallet; - invalidHashWallet.transactionHash = QStringLiteral("not-a-hash"); - FakeAmmClient invalidHashClient; - invalidHashClient.requiresFreshLp = false; - NewPositionRuntime invalidHashRuntime(&invalidHashWallet, &invalidHashClient); - const QVariantMap invalidHash = invalidHashRuntime.submit( - request, QStringLiteral("sha256:expected"), readyNetwork(), true); - if (!expect(invalidHash.value(QStringLiteral("code")).toString() - == QStringLiteral("wallet_submission_failed") - && !invalidHash.contains(QStringLiteral("transactionId")), - "invalid native hash should fail without a hex fallback")) - return 1; - if (!expect(invalidHashWallet.submissions == 1, - "hash conversion should happen after wallet submission")) - return 1; - - FakeWallet staleWallet; - FakeAmmClient staleClient; - staleClient.quoteHash = QStringLiteral("sha256:changed"); - NewPositionRuntime staleRuntime(&staleWallet, &staleClient); - const QVariantMap stale = staleRuntime.submit( - request, QStringLiteral("sha256:expected"), readyNetwork(), true); - if (!expect(stale.value(QStringLiteral("code")).toString() - == QStringLiteral("quote_changed"), - "changed quote should stop submission")) - return 1; - if (!expect(staleWallet.createdAccounts == 0 && staleWallet.submissions == 0, - "stale quote should have no wallet side effects")) - return 1; - - return 0; -} diff --git a/flake.nix b/flake.nix index 1fa131a8..16d39651 100644 --- a/flake.nix +++ b/flake.nix @@ -108,19 +108,24 @@ } ); - # The AMM QML UI module (apps/amm). Its external_libraries entry - # (amm_client) is resolved to `self.packages.${system}.amm_client` - # above — the module builder's resolveExtInput reads - # `flakeInput.packages.${system}.${pkgName}`, and passing `self` here - # works because `self` is this very flake, which already exposes that - # package per crateOutputs above. + # The AMM QML UI module (apps/amm). It links no amm_client library of its + # own — the AMM logic lives in the amm_module core module, which the UI + # depends on (declared in apps/amm/metadata.json, reached via + # modules().amm_module in the backend) alongside the logos_execution_zone + # wallet module. appOutputs = logos-module-builder.lib.mkLogosQmlModule { src = ./apps/amm; configFile = ./apps/amm/metadata.json; - flakeInputs = inputs; - externalLibInputs = { - amm_client = { input = self; packages.default = "amm_client"; }; - }; + # amm_module isn't a flake input — it's built from this same flake below + # (ammModuleOutputs) — so inject it into flakeInputs under its dependency + # name so the builder's dependency resolver finds it (reads its .lidl to + # generate the modules().amm_module wrapper). The wallet module stays a + # direct dep too, so both the UI and amm_module resolve the one shared + # wallet instance. + flakeInputs = inputs // { amm_module = ammModuleOutputs; }; + # The UI links no external lib of its own — the AMM brain (amm_client) is + # linked by amm_module, which the UI reaches via modules().amm_module. + externalLibInputs = { }; # The AMM UI links the shared C++ wallet access lib and bundles the # Logos.Wallet QML module (apps/shared/wallet). apps/amm/flake.nix wires # these via its `shared_wallet` input; when built from this root flake @@ -153,6 +158,22 @@ appApps = appOutputs.apps or { }; appPkgs = appOutputs.packages or { }; + # AMM core module (modules/amm): the AMM business logic as a headless + # `core` Logos module. It links the amm_client crate (the transport- + # independent AMM brain, resolved via `self`) and depends on the + # logos_execution_zone wallet module (declared in modules/amm/metadata.json, + # reached via modules().logos_execution_zone in the impl). Exposed as the + # `amm-module` package; no UI/app output. + ammModuleOutputs = logos-module-builder.lib.mkLogosModule { + src = ./modules/amm; + configFile = ./modules/amm/metadata.json; + flakeInputs = inputs; + externalLibInputs = { + amm_client = { input = self; packages.default = "amm_client"; }; + }; + }; + ammModulePkgs = ammModuleOutputs.packages or { }; + # Wrap the app launcher to export DYLD_FALLBACK_LIBRARY_PATH pointing at the # amm_client lib. The logos module builder links the plugin against # @rpath/libamm_client.dylib but does NOT stage that dylib into the @@ -180,10 +201,13 @@ system: cratePkgs: let appSysPkgs = appPkgs.${system} or { }; + ammModSysPkgs = ammModulePkgs.${system} or { }; in (builtins.removeAttrs cratePkgs [ "default" ]) // (builtins.removeAttrs appSysPkgs [ "default" ]) // (if appSysPkgs ? default then { amm-ui = appSysPkgs.default; } else { }) + // (builtins.removeAttrs ammModSysPkgs [ "default" ]) + // (if ammModSysPkgs ? default then { amm-module = ammModSysPkgs.default; } else { }) ) crateOutputs.packages; in (builtins.removeAttrs appOutputs [ "apps" "packages" ]) diff --git a/modules/amm/CMakeLists.txt b/modules/amm/CMakeLists.txt new file mode 100644 index 00000000..817ab566 --- /dev/null +++ b/modules/amm/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.14) +project(AmmModulePlugin LANGUAGES CXX) + +# Include the Logos Module CMake helper (provided by logos-module-builder). +if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) + include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) +elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake") + include(cmake/LogosModule.cmake) +else() + message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.") +endif() + +# Derive the module name from metadata.json — single source of truth. +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/metadata.json" METADATA_JSON) +string(JSON MODULE_NAME GET ${METADATA_JSON} name) + +# Universal core module: we write only the impl class; the Qt plugin glue is +# generated from src/amm_module_impl.h (because metadata.json sets +# "interface": "universal"). EXTERNAL_LIBS links amm_client — the pure-Rust, +# transport-independent AMM brain (PDA/decode/quote/plan/encode) exposed as a +# JSON FFI (amm_client.h), the same library the UI links. The chain I/O +# dependency (logos_execution_zone) is declared in metadata.json and reached via +# modules().logos_execution_zone in the impl, so it is NOT listed here. +logos_module( + NAME ${MODULE_NAME} + SOURCES + src/amm_module_impl.h + src/amm_module_impl.cpp + EXTERNAL_LIBS + amm_client +) diff --git a/modules/amm/README.md b/modules/amm/README.md new file mode 100644 index 00000000..d42d8ac5 --- /dev/null +++ b/modules/amm/README.md @@ -0,0 +1,260 @@ +# AMM core module + +`amm_module` — the AMM business logic as a **headless Logos `core` module** +(`interface: "universal"`). It exposes the AMM's on-chain operations so they can +be driven identically from the QML UI (`apps/amm`, via the generated +`modules().amm_module` caller) and from the CLI (`logoscore call amm_module …`). + +See the [Logos developer guide](https://github.com/logos-co/logos-tutorial/blob/master/logos-developer-guide.md) +for the module framework. + +## What it does + +The impl class `AmmModuleImpl` (`src/amm_module_impl.{h,cpp}`) is a **transport +adapter**: the AMM domain math lives in the Rust `amm_client` crate (a +transport-independent JSON FFI), and this module sequences those pure ops with +chain I/O delegated to the `logos_execution_zone` wallet module. Its public +methods (the module API is generated from the header) are: + +- `resolvePool(defAHex, defBHex)` — derives the pool PDAs + (config/pool/vaults/current-tick) and reads the pool's on-chain reserves. + Returns `{ exists: false, error }` when the AMM isn't configured/initialized or + the pool has no liquidity. +- `swapExactInput(defAHex, defBHex, userInputHoldingHex, userOutputHoldingHex, amountIn, minOut, deadline)` + — 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. +- `newPositionContext(request, walletOpen, refreshWalletAccounts)` — the + add-liquidity view state (available tokens, fee tiers, warnings) as a + `new-position.v1` map. +- `quoteNewPosition(request, walletOpen)` — prices an add-liquidity request + against current on-chain state (read-only). +- `submitNewPosition(request, quoteHash, walletOpen, freshLpId)` — submits an + add-liquidity transaction. When the quote needs a fresh LP holding and + `freshLpId` is empty, returns `{ status: "requires_fresh_lp" }` **without** + submitting: the caller (the app backend, which owns the wallet keyset) creates + the account and calls again with its id. Headless callers pre-create an LP + holding and pass it. + +## How it fits together + +``` +QML ──modules().amm_module──┐ ┌── amm_client (Rust cdylib, JSON FFI): +CLI ──logoscore call────────┤ │ PDA derivation, account decode, + ▼ │ quote/plan math, instruction encoding + amm_module ───┤ — transport-independent (external_libraries) + │ │ + │ └── logos_execution_zone (dependency): + ▼ chain reads + tx submit + base58, + via modules().logos_execution_zone.* +``` + +The `amm_client` crate is deliberately I/O-free — each op takes the account data +it needs as JSON input and returns a JSON result. This module is the transport +adapter the crate is designed to require: it fetches accounts through the wallet +module (`get_account_public`, `list_accounts`), hands them to the pure Rust op, +and submits the plan the op returns (`send_generic_public_transaction`). It reads +the **same** shared wallet instance the UI opened (Basecamp loads core modules as +singletons; standalone the LogosAPI client cache dedups the connection), so it +never opens a second wallet. + +The impl is deliberately **Qt-free** (`std::string` / `LogosMap` / `LogosList` / +`nlohmann::json`), as the universal authoring model requires. + +## 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 `. + +**Amounts (`amountIn`/`minOut`, u128) and `deadline` (u64 unix-ms)** are declared +`nlohmann::json`, so each accepts **either a JSON number or a decimal string**: + +- **small integer** → pass it bare: `1000` +- **big value** (an amount above the JSON/int64 range, e.g. `1e18` base units for + an 18-decimal token, **and** the unix-ms deadline, which is always large) → + pass it as a **quote-wrapped string**: `'"1000000000000000000"'` + +Why the split: `logoscore` promotes any bare number past ~2³¹ to a JSON *double*, +which can't hold a large integer exactly. The module therefore **rejects JSON +floats** (rather than submit a silently-rounded amount) and requires big values +as strings, which are bit-exact. A quoted CLI arg (`'"…"'`) reaches the module +with the quotes folded into the value; the string branch strips that wrapper. +The UI passes `QString` (→ `QVariant` → string branch) and is unaffected. + +## Build + +Built from the **repo-root** flake (which provides the `amm_client` library it +links): + +```bash +nix build .#amm-module +# output: result/lib/amm_module_plugin.dylib (+ libamm_client.dylib) +``` + +## Runtime configuration + +Both are absolute-path env vars set on the **process that hosts the module** +(the `logoscore` daemon, or Basecamp) — not on the `call`: + +- `AMM_PROGRAM_BIN` — the deployed `amm.bin`. Required; its ELF determines the + program id and every derived PDA. Without it, `resolvePool` returns + `{ exists: false, error: "no_program_bin" }`. +- `TOKENS_CONFIG` — JSON array of `{ symbol, name, definitionId, holding, decimals }` + consumed by `tokenList()`. + +## Headless usage with `logoscore` + +### Prerequisites + +Have all of the following in place before staging the modules dir: + +1. **Nix** with flakes enabled (same as the rest of the repo). + +2. **The runtime CLIs** (from their own flakes, per the developer guide): + + ```bash + nix profile install 'github:logos-co/logos-logoscore-cli' # logoscore (daemon + client) + nix build 'github:logos-co/logos-module#lm' # lm (static plugin inspector, optional) + ``` + + `lm` introspects a built plugin without running it — handy to confirm the API + (`lm result/lib/amm_module_plugin.dylib` shows methods, signatures, deps). + +3. **This module, built** (produces `amm_module_plugin.dylib` + `libamm_client.dylib`): + + ```bash + nix build .#amm-module # from the repo root; output under result/lib/ + ``` + +4. **The wallet module it depends on, built** — `logos_execution_zone` is a + *separate repo*, not part of this tree. Build the **same rev** this module + pins as its `logos_execution_zone` flake input (mismatched revs = ABI/ImageID + drift), producing `logos_execution_zone_plugin.dylib` + `libwallet_ffi.dylib`: + + ```bash + nix build 'github:gravityblast/logos-execution-zone-module?ref=fix/generic-tx-instruction-bstr' + # output under result/lib/ — copy it aside before building amm-module (both use ./result) + ``` + +5. **The deployed `amm.bin`** for `AMM_PROGRAM_BIN` — the exact binary running on + 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` + pointing at your sequencer, plus `storage.json` with your accounts), and a + **running sequencer** with the AMM initialized and a pool holding liquidity. + Swaps also need the wallet **open + synced** (see below); reads + (`resolvePool`/`tokenList`) do not. + +### Staging the modules directory + +**Core modules get no `.lgx` from the builder** (only UI modules do), so stage a +modules directory by hand — one subdir per module, each with `manifest.json` + +`variant` + the plugin dylib (and its sibling FFI dylib, since rpath is +`@loader_path`). The daemon discovers modules from this layout; it does **not** +verify the manifest hashes at load time. + +``` +modules/ + amm_module/ + amm_module_plugin.dylib + libamm_client.dylib + variant # one line: darwin-arm64-dev + manifest.json + logos_execution_zone/ + logos_execution_zone_plugin.dylib + libwallet_ffi.dylib + variant + manifest.json +``` + +`amm_module/manifest.json`: + +```json +{ + "name": "amm_module", "type": "core", "version": "0.1.0", + "manifestVersion": "0.2.0", "dependencies": ["logos_execution_zone"], + "main": { "darwin-arm64-dev": "amm_module_plugin.dylib" } +} +``` + +Start the daemon **with the env vars set on it**, then load the dependency +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 +``` + +`resolvePool`/`tokenList` are read-only: + +```bash +logoscore call amm_module tokenList +logoscore call amm_module resolvePool +``` + +`swapExactInput` needs an **open, synced wallet** pointed at your sequencer +(nothing opens it for you headlessly), then the swap. Note the amount/deadline +conventions above — small amount bare, deadline (and any big amount) quoted: + +```bash +logoscore call logos_execution_zone open ~/.lee/wallet/wallet_config.json ~/.lee/wallet/storage.json +logoscore call amm_module swapExactInput \ + \ + 1000 1 '"32503680000000"' +# big amount: replace 1000 with '"1000000000000000000"' +``` + +### Debugging + +Set `AMM_DEBUG=1` on the daemon to trace every `swapExactInput` step (parsed +args, the assembled account list, and the raw `send_generic_public_transaction` +reply) to the module host's stderr, which the daemon captures in its log. A +failed swap returns an empty tx hash; the trace shows the reason. + +### Wallet sync gotcha + +`get_balance` reads the wallet's *local synced state*, and `swapExactInput` +builds transactions against it. If you reset/reinitialize the sequencer, the +wallet's `storage.json` may keep a stale `last_synced_block` ahead of the new +chain — transactions then reference dead state and the sequencer rejects them +(reserves don't move). Reset the cursor (`last_synced_block: 0`, keep +`key_chain`/`labels`) and re-`open` + `sync_to_block ` to re-sync from +genesis. `resolvePool` is unaffected — it reads live from the sequencer. + +## Install into Basecamp + +`amm_module` is a **core** module (installed into `modules/`, alongside the +wallet module), not a UI plugin. Build its `.lgx` from the root flake and +install with `lgpm --modules-dir …` (see `apps/amm/README.md` for the full +three-package flow: `logos_execution_zone` + `amm_module` + the `amm_ui` UI +plugin). + +## QtRO / byte-string note + +`send_generic_public_transaction` takes `instruction` as a byte string +(`std::vector`). This module sends the plan's u32 words as their +little-endian bytes and references the program by its id hex (not the raw ELF). +It requires the wallet module built with the byte-string `instruction` param — +the fork pinned as the `logos_execution_zone` input. See +`docs/amm-swap-qtro-serialization-bug.md`. + +## Known follow-ups + +- **`swapExactOutput` is not exposed yet.** The on-chain program supports it + (`amm_core::Instruction::SwapExactOutput`, identical account layout to + `SwapExactInput`), but the client path was only ever built for exact-input: + `amm_client` has no exact-output op and neither the UI nor this module has a + `swapExactOutput` method. Adding it is a near-copy of the exact-input path — an + `amm_swap_exact_output_*` op in the crate plus a `swapExactOutput` method here. diff --git a/modules/amm/flake.nix b/modules/amm/flake.nix new file mode 100644 index 00000000..2a588138 --- /dev/null +++ b/modules/amm/flake.nix @@ -0,0 +1,29 @@ +{ + description = "Logos AMM core module — headless AMM business logic (pool resolution + swaps)"; + + inputs = { + logos-module-builder.url = "github:logos-co/logos-module-builder"; + + # Core wallet module dependency. The input name must match the + # metadata.json `dependencies` entry so the builder resolves it as a module + # dependency. Same fork the repo-root flake pins (the QtRO byte-string + # `instruction` fix for send_generic_public_transaction). + logos_execution_zone.url = "github:gravityblast/logos-execution-zone-module?ref=fix/generic-tx-instruction-bstr"; + }; + + # NOTE: like apps/amm, this flake is NOT built standalone. The amm_client + # crate this module links (the Rust JSON-FFI brain) lives in the repo-root + # flake, and referencing it from here would require a hardcoded `git+file://` + # path or a `path:../..` input — the latter fails flake evaluation because this + # dir is copied into the Nix store as its own flake root, so `../..` can't + # escape it. Instead, the repo-root flake.nix builds this module directly + # (src = ./modules/amm) and resolves amm_client via `self`. Build it from + # the repo root: + # nix build .#amm-module + outputs = inputs@{ logos-module-builder, ... }: + logos-module-builder.lib.mkLogosModule { + src = ./.; + configFile = ./metadata.json; + flakeInputs = inputs; + }; +} diff --git a/modules/amm/metadata.json b/modules/amm/metadata.json new file mode 100644 index 00000000..90096063 --- /dev/null +++ b/modules/amm/metadata.json @@ -0,0 +1,26 @@ +{ + "name": "amm_module", + "version": "0.1.0", + "type": "core", + "interface": "universal", + "category": "amm", + "description": "AMM business logic — on-chain pool resolution and swaps", + "main": "amm_module_plugin", + "dependencies": ["logos_execution_zone"], + + "nix": { + "packages": { + "build": [], + "runtime": [] + }, + "external_libraries": [ + { "name": "amm_client" } + ], + "cmake": { + "find_packages": [], + "extra_sources": [], + "extra_include_dirs": [], + "extra_link_libraries": [] + } + } +} diff --git a/modules/amm/src/amm_module_impl.cpp b/modules/amm/src/amm_module_impl.cpp new file mode 100644 index 00000000..6dbda1be --- /dev/null +++ b/modules/amm/src/amm_module_impl.cpp @@ -0,0 +1,742 @@ +#include "amm_module_impl.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// Generated at build time by logos-cpp-generator. Defines `LogosModules` with +// one std-typed accessor per metadata.json dependency — here +// `logos_execution_zone`. Included only in the .cpp so the impl header the +// generator parses stays free of Qt and codegen types. +#include "logos_sdk.h" + +extern "C" { +#include "amm_client.h" +} + +namespace { + +using json = nlohmann::json; + +// AMM_DEBUG-gated tracing. The module runs in its own logos_host process whose +// stderr the daemon captures, so these lines surface in the daemon log. +bool ammDebug() { + static const bool on = std::getenv("AMM_DEBUG") != nullptr; + return on; +} +#define AMM_TRACE(msg) \ + do { \ + if (ammDebug()) std::cerr << "[amm-debug] " << msg << std::endl; \ + } while (0) + +// Absolute path to the deployed AMM program's compiled binary (amm.bin). The +// module can't derive this itself: the wallet module's bundled AMM program may +// differ from whatever is deployed on the target sequencer, and the bytes are +// 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"; + +// new-position.v1 response schema tag (matches the app-side NewPositionRuntime +// and the Rust client's NEW_POSITION_SCHEMA). +constexpr char SCHEMA[] = "new-position.v1"; + +int hexVal(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +// True when `s` is exactly `len` hex digits (case-insensitive). +bool isHexLen(const std::string& s, size_t len) { + if (s.size() != len) return false; + for (const char c : s) + if (hexVal(c) < 0) return false; + return true; +} + +// True when `s` is an even-length run of hex digits (case-insensitive). +bool isHexEven(const std::string& s) { + if (s.size() % 2 != 0) return false; + for (const char c : s) + if (hexVal(c) < 0) return false; + return true; +} + +std::string toHex(const uint8_t* p, size_t n) { + static const char* const kDigits = "0123456789abcdef"; + std::string s; + s.reserve(n * 2); + for (size_t i = 0; i < n; ++i) { + s.push_back(kDigits[p[i] >> 4]); + s.push_back(kDigits[p[i] & 0x0f]); + } + return s; +} + +// Exception-safe field accessor over a json object: returns "" when the key is +// missing or not a string (nlohmann's value()/get() throw on a type mismatch). +std::string jStr(const json& obj, const char* key) { + const auto it = obj.find(key); + return (it != obj.end() && it->is_string()) ? it->get() : std::string(); +} + +// Milliseconds since the unix epoch (u64). Used for the plan's `nowMs` and the +// client deadline check — the module runs on the host, not in the zkVM, so wall +// clock is available (unlike a guest). +uint64_t nowMs() { + return static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count()); +} + +// Decimal string -> u64. False (leaving `out` unset) on empty, non-digit, or +// overflow. +bool parseU64(const std::string& s, uint64_t& out) { + if (s.empty()) return false; + uint64_t value = 0; + for (const char c : s) { + if (c < '0' || c > '9') return false; + const uint64_t d = static_cast(c - '0'); + if (value > (~static_cast(0) - d) / 10) return false; + value = value * 10 + d; + } + out = value; + return true; +} + +// Coerce a swap amount arg — arriving as EITHER a JSON number (bare `1000` on +// the CLI) or a decimal string ("1000", a big u128 the UI passes, or a +// quote-wrapped big value on the CLI) — to its canonical decimal-string form. +// Small values fit a JSON integer; big values (amounts above the JSON/int64 +// range, and unix-ms deadlines) must be strings. Returns false for JSON floats, +// negatives, or non-numeric strings, leaving `out` unset. +bool jsonAmountToDecimal(const json& j, std::string& out) { + if (j.is_string()) { + std::string s = j.get(); + // Trim whitespace, then a single pair of wrapping double quotes if + // present. This is what lets an EXACT u128 above 2^53 be passed from the + // logoscore CLI: such a value can't be a JSON number (a bare number that + // large is a lossy double), so it must be a string — but the CLI folds a + // quoted arg's quotes INTO the value (`"1e18"` arrives as the literal + // chars "\"1e18\""). Stripping the wrapper recovers the clean digits. A + // clean string from the UI (no wrapping quotes) is unaffected. + auto trim = [](std::string& x) { + const size_t a = x.find_first_not_of(" \t\n\r"); + const size_t b = x.find_last_not_of(" \t\n\r"); + x = (a == std::string::npos) ? std::string() : x.substr(a, b - a + 1); + }; + trim(s); + if (s.size() >= 2 && s.front() == '"' && s.back() == '"') { + s = s.substr(1, s.size() - 2); + trim(s); + } + out = s; + return true; + } + if (j.is_number_unsigned()) { + out = std::to_string(j.get()); + return true; + } + if (j.is_number_integer()) { + const int64_t v = j.get(); + if (v < 0) return false; + out = std::to_string(v); + return true; + } + // Reject JSON floats (and null/object/array). logoscore promotes any bare + // number beyond ~2^31 to a double, so a large bare value arrives here as a + // float — already rounded upstream. Rather than silently accept it, require + // such big numbers as a (quote-wrapped) STRING, which is bit-exact. + return false; +} + +// json string/bool arrays -> std vectors, and a u32-word array -> the +// little-endian bytes the wallet module's byte-string `instruction` expects. +std::vector jsonStrVec(const json& arr) { + std::vector out; + if (!arr.is_array()) return out; + out.reserve(arr.size()); + for (const auto& v : arr) + if (v.is_string()) out.push_back(v.get()); + return out; +} + +std::vector jsonBoolVec(const json& arr) { + std::vector out; + if (!arr.is_array()) return out; + out.reserve(arr.size()); + for (const auto& v : arr) + out.push_back(v.is_boolean() && v.get()); + return out; +} + +std::vector jsonWordsToLeBytes(const json& arr) { + std::vector out; + if (!arr.is_array()) return out; + out.reserve(arr.size() * sizeof(uint32_t)); + for (const auto& v : arr) { + const uint32_t word = v.is_number() ? static_cast(v.get()) : 0; + out.push_back(static_cast(word & 0xff)); + out.push_back(static_cast((word >> 8) & 0xff)); + out.push_back(static_cast((word >> 16) & 0xff)); + out.push_back(static_cast((word >> 24) & 0xff)); + } + return out; +} + +// Result of an amm_client JSON op: the `{ ok, value }` envelope decoded. +struct FfiResult { + bool ok = false; + json value; +}; + +// Serialize `request`, hand it to an amm_client op, and decode its +// `{ ok, value, error }` envelope. Mirrors apps/amm/src/AmmClient.cpp. `value` +// is only populated (and `ok` true) when the op reports success with an object. +FfiResult call(char* (*op)(const char*), const json& request) { + const std::string payload = request.dump(); + char* raw = op(payload.c_str()); + if (raw == nullptr) { + AMM_TRACE("amm_client op returned null"); + return {}; + } + const std::string response(raw); + amm_free(raw); + + const auto doc = json::parse(response, nullptr, /*allow_exceptions=*/false); + if (!doc.is_object()) { + AMM_TRACE("amm_client op returned invalid JSON"); + return {}; + } + if (!doc.value("ok", false)) { + AMM_TRACE("amm_client op failure: " << doc.value("error", std::string())); + return {}; + } + const auto it = doc.find("value"); + if (it == doc.end() || !it->is_object()) { + AMM_TRACE("amm_client op value is not an object"); + return {}; + } + return {true, *it}; +} + +// new-position.v1 envelope builders (ported from NewPositionRuntime). +json issue(const std::string& code, const json& blockingFields = json::array()) { + return { + {"code", code}, + {"recoverable", true}, + {"blockingFields", blockingFields}, + {"details", json::object()}, + }; +} + +json publicError(const std::string& code, + const json& blockingFields = json::array(), + const json& details = json::object()) { + json error = issue(code, blockingFields); + error["details"] = details; + return { + {"schema", SCHEMA}, + {"status", "error"}, + {"canSubmit", false}, + {"code", code}, + {"errors", json::array({error})}, + {"warnings", json::array()}, + {"accountPreview", json::array()}, + }; +} + +json contextState(const std::string& status, + const std::string& network_id, + const std::string& network_fingerprint, + const std::string& code = {}) { + json state = { + {"schema", SCHEMA}, + {"status", status}, + {"networkId", network_id}, + {"networkFingerprint", network_fingerprint}, + {"tokens", json::array()}, + {"feeTiers", json::array()}, + {"warnings", json::array()}, + }; + if (!code.empty()) state["code"] = code; + return state; +} + +// A json array of the strings at `obj[key]` (empty array when absent/wrong type). +json stringArray(const json& obj, const char* key) { + const auto it = obj.find(key); + if (it == obj.end() || !it->is_array()) return json::array(); + json out = json::array(); + for (const auto& v : *it) + if (v.is_string()) out.push_back(v); + return out; +} + +} // namespace + +std::vector AmmModuleImpl::loadAmmElf() { + const char* path = std::getenv(AMM_PROGRAM_BIN_ENV); + if (path == nullptr || *path == '\0') return {}; + std::ifstream file(path, std::ios::binary); + if (!file) return {}; + return std::vector((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); +} + +std::string AmmModuleImpl::ammProgramId() { + const std::vector elf = loadAmmElf(); + if (elf.empty()) return {}; + // Hand the deployed binary to the amm_client program_id op, which decodes it + // and computes the Image ID — 64-char lowercase hex, little-endian per u32 + // word (matches `spel program-id` and the on-chain *_program_id fields). + const FfiResult r = call(amm_program_id, json{{"elf", toHex(elf.data(), elf.size())}}); + if (!r.ok) { + AMM_TRACE("ammProgramId: amm_program_id op failed"); + return {}; + } + return jStr(r.value, "programId"); +} + +AmmModuleImpl::Network AmmModuleImpl::network() { + Network net; + net.amm_program_id = ammProgramId(); + // The program id changes per deployment, so it doubles as the network + // fingerprint (a quote can't be replayed against a different program). + net.fingerprint = net.amm_program_id; + for (const auto& token : tokenList()) { + const std::string id = jStr(token, "definitionId"); + if (!id.empty()) net.token_ids.push_back(id); + } + net.status = net.amm_program_id.empty() ? "config_missing" : "ready"; + return net; +} + +std::string AmmModuleImpl::normalizeAccountId(const std::string& id) { + size_t start = 0; + size_t end = id.size(); + while (start < end && std::isspace(static_cast(id[start]))) ++start; + while (end > start && std::isspace(static_cast(id[end - 1]))) --end; + std::string t = id.substr(start, end - start); + + if (isHexLen(t, 64)) { + std::transform(t.begin(), t.end(), t.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return t; + } + + // Try base58 -> hex via the wallet module ("" on failure). + std::string hex = modules().logos_execution_zone.account_id_from_base58(t); + std::transform(hex.begin(), hex.end(), hex.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return hex; +} + +nlohmann::json AmmModuleImpl::readPublicAccount(const std::string& account_id) { + // Matches the app-side accountReadJson: WalletAccountRead defaults status to + // "read_failed" and only flips to "ok" when every field is well-formed hex; + // the `account` key is present only for an ok read. + json result = {{"id", account_id}, {"status", "read_failed"}}; + + const std::string account_json = + modules().logos_execution_zone.get_account_public(account_id); + const auto obj = json::parse(account_json, nullptr, /*allow_exceptions=*/false); + if (!obj.is_object()) return result; + + const std::string owner = jStr(obj, "program_owner"); + const std::string balance = jStr(obj, "balance"); + const std::string nonce = jStr(obj, "nonce"); + const std::string data = jStr(obj, "data"); + if (!isHexLen(owner, 64) || !isHexLen(balance, 32) || !isHexLen(nonce, 32) + || !isHexEven(data)) { + return result; + } + + result["status"] = "ok"; + result["account"] = { + {"program_owner", owner}, + {"balance", balance}, + {"nonce", nonce}, + {"data", data}, + }; + return result; +} + +nlohmann::json AmmModuleImpl::walletAccountReads(bool wallet_open, bool /*refresh*/) { + json out = json::array(); + if (!wallet_open) return out; + + // Normalize the wallet module's [any] return (a vector or a json array) + // through json so we can iterate/type-check it uniformly. + json accounts = modules().logos_execution_zone.list_accounts(); + if (!accounts.is_array()) return out; + + for (const auto& entry : accounts) { + if (!entry.is_object()) continue; + // The AMM path uses only public accounts (LP holdings / token balances). + if (!entry.value("is_public", true)) continue; + const std::string id = jStr(entry, "account_id"); + if (id.empty()) continue; + out.push_back(readPublicAccount(id)); + } + return out; +} + +nlohmann::json AmmModuleImpl::readConfig(const Network& net) { + const FfiResult configResult = + call(amm_config_id, json{{"ammProgramId", net.amm_program_id}}); + if (!configResult.ok) return json(); // null: config_id op failed + return readPublicAccount(jStr(configResult.value, "configId")); +} + +LogosMap AmmModuleImpl::resolvePool(const std::string& def_a_hex, + const std::string& def_b_hex) { + const json absent = {{"exists", false}}; + + const Network net = network(); + if (net.status != "ready") return absent; + + const json config = readConfig(net); + if (config.is_null()) return absent; + + const FfiResult pairResult = call(amm_swap_pair, json{ + {"ammProgramId", net.amm_program_id}, + {"tokenInId", def_a_hex}, + {"tokenOutId", def_b_hex}, + {"config", config}, + }); + if (!pairResult.ok || jStr(pairResult.value, "status") != "ok") return absent; + + const json pool = readPublicAccount(jStr(pairResult.value, "poolId")); + const FfiResult resolveResult = call(amm_resolve_pool, json{{"pool", pool}}); + if (!resolveResult.ok) return absent; + return resolveResult.value; +} + +std::string AmmModuleImpl::swapExactInput(const std::string& def_a_hex, + const std::string& def_b_hex, + const std::string& user_input_holding_hex, + const std::string& user_output_holding_hex, + const nlohmann::json& amount_in, + const nlohmann::json& min_out, + const nlohmann::json& deadline) { + std::string amount_in_decimal; + std::string min_out_decimal; + std::string deadline_decimal; + if (!jsonAmountToDecimal(amount_in, amount_in_decimal) + || !jsonAmountToDecimal(min_out, min_out_decimal) + || !jsonAmountToDecimal(deadline, deadline_decimal)) { + AMM_TRACE("swapExactInput: FAIL amount/deadline not a number or decimal string"); + return {}; + } + + const Network net = network(); + if (net.status != "ready") { + AMM_TRACE("swapExactInput: FAIL network not ready (" << net.status << ")"); + return {}; + } + + const json config = readConfig(net); + if (config.is_null()) { + AMM_TRACE("swapExactInput: FAIL config_id op failed"); + return {}; + } + + // amm_swap_plan resolves the pool, reorders holdings to the pool's canonical + // def order, encodes SwapExactInput, and returns a ready-to-submit plan. + const FfiResult planResult = call(amm_swap_plan, json{ + {"ammProgramId", net.amm_program_id}, + {"tokenInId", def_a_hex}, + {"tokenOutId", def_b_hex}, + {"config", config}, + {"userInputHoldingId", user_input_holding_hex}, + {"userOutputHoldingId", user_output_holding_hex}, + {"amountIn", amount_in_decimal}, + {"minOut", min_out_decimal}, + {"deadlineMs", deadline_decimal}, + }); + if (!planResult.ok || jStr(planResult.value, "status") != "ready") { + AMM_TRACE("swapExactInput: FAIL amm_swap_plan not ready"); + return {}; + } + const json plan = planResult.value; + + const std::vector accounts = jsonStrVec(plan.value("accountIds", json::array())); + const std::vector signers = jsonBoolVec(plan.value("signingRequirements", json::array())); + const std::vector instruction = jsonWordsToLeBytes(plan.value("instruction", json::array())); + const std::string program_id = jStr(plan, "programId"); + + AMM_TRACE("swapExactInput: SUBMIT programId=" << program_id + << " instrBytes=" << instruction.size() << " accounts=" << accounts.size()); + + const std::string reply = modules().logos_execution_zone.send_generic_public_transaction( + accounts, signers, instruction, program_id); + AMM_TRACE("swapExactInput: tx reply=" << reply); + + const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false); + if (!obj.is_object() || !obj.value("success", false)) { + AMM_TRACE("swapExactInput: FAIL tx not successful"); + return {}; + } + return 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; + + json token; + token["symbol"] = jStr(entry, "symbol"); + token["name"] = jStr(entry, "name"); + token["definitionId"] = definition_id; + token["holding"] = holding; + token["decimals"] = entry.value("decimals", 0); + out.push_back(token); + } + return out; +} + +nlohmann::json AmmModuleImpl::buildQuoteInput(const LogosMap& request, + const Network& net, + bool wallet_open, + bool fresh_wallet_accounts, + nlohmann::json* error) { + if (net.status != "ready") { + *error = publicError(net.status); + return json(); + } + const FfiResult configResult = + call(amm_config_id, json{{"ammProgramId", net.amm_program_id}}); + if (!configResult.ok) { + *error = publicError("backend_error"); + return json(); + } + const json config = readPublicAccount(jStr(configResult.value, "configId")); + + const FfiResult pairResult = call(amm_pair_ids, json{ + {"ammProgramId", net.amm_program_id}, + {"config", config}, + {"tokenAId", request.value("tokenAId", json())}, + {"tokenBId", request.value("tokenBId", json())}, + }); + if (!pairResult.ok) { + *error = publicError("backend_error"); + return json(); + } + const json pairManifest = pairResult.value; + if (jStr(pairManifest, "status") != "ok") { + *error = publicError(jStr(pairManifest, "code")); + return json(); + } + + const json walletAccounts = walletAccountReads(wallet_open, fresh_wallet_accounts); + const json snapshot = { + {"config", config}, + {"tokenA", readPublicAccount(jStr(pairManifest, "tokenAId"))}, + {"tokenB", readPublicAccount(jStr(pairManifest, "tokenBId"))}, + {"pool", readPublicAccount(jStr(pairManifest, "poolId"))}, + {"vaultA", readPublicAccount(jStr(pairManifest, "vaultAId"))}, + {"vaultB", readPublicAccount(jStr(pairManifest, "vaultBId"))}, + {"lpDefinition", readPublicAccount(jStr(pairManifest, "lpDefinitionId"))}, + {"lpLockHolding", readPublicAccount(jStr(pairManifest, "lpLockHoldingId"))}, + {"currentTick", readPublicAccount(jStr(pairManifest, "currentTickId"))}, + {"clock", readPublicAccount(jStr(pairManifest, "clockId"))}, + {"walletAvailable", wallet_open}, + {"walletAccounts", walletAccounts}, + }; + return { + {"networkId", net.id}, + {"networkFingerprint", net.fingerprint}, + {"ammProgramId", net.amm_program_id}, + {"request", request}, + {"snapshot", snapshot}, + }; +} + +LogosMap AmmModuleImpl::newPositionContext(const LogosMap& request, + bool wallet_open, + bool refresh_wallet_accounts) { + const Network net = network(); + if (net.status != "ready") + return contextState(net.status, net.id, net.fingerprint); + + const json walletAccounts = walletAccountReads(wallet_open, refresh_wallet_accounts); + + const FfiResult configResult = + call(amm_config_id, json{{"ammProgramId", net.amm_program_id}}); + if (!configResult.ok) + return contextState("error", net.id, net.fingerprint, "backend_error"); + const json config = readPublicAccount(jStr(configResult.value, "configId")); + + json configured = json::array(); + for (const auto& id : net.token_ids) configured.push_back(id); + const json recent = stringArray(request, "recentTokenIds"); + const json resolved = stringArray(request, "resolvedTokenIds"); + + const FfiResult tokenResult = call(amm_token_ids, json{ + {"ammProgramId", net.amm_program_id}, + {"config", config}, + {"walletAccounts", walletAccounts}, + {"configuredTokenIds", configured}, + {"recentTokenIds", recent}, + {"resolvedTokenIds", resolved}, + }); + const json tokenManifest = tokenResult.value; + if (!tokenResult.ok || jStr(tokenManifest, "status") != "ok") { + const std::string code = + tokenResult.ok ? jStr(tokenManifest, "code") : std::string("backend_error"); + return contextState("error", net.id, net.fingerprint, + code.empty() ? "backend_error" : code); + } + + json definitions = json::array(); + for (const auto& id : tokenManifest.value("tokenIds", json::array())) + if (id.is_string()) definitions.push_back(readPublicAccount(id.get())); + + const FfiResult contextResult = call(amm_context, json{ + {"networkId", net.id}, + {"networkFingerprint", net.fingerprint}, + {"ammProgramId", net.amm_program_id}, + {"walletAvailable", wallet_open}, + {"config", config}, + {"walletAccounts", walletAccounts}, + {"tokenDefinitions", definitions}, + {"configuredTokenIds", configured}, + {"recentTokenIds", recent}, + {"resolvedTokenIds", resolved}, + }); + return contextResult.ok + ? contextResult.value + : contextState("error", net.id, net.fingerprint, "backend_error"); +} + +LogosMap AmmModuleImpl::quoteNewPosition(const LogosMap& request, bool wallet_open) { + const Network net = network(); + json error; + const json input = buildQuoteInput(request, net, wallet_open, /*fresh=*/false, &error); + if (!error.is_null()) return error; + + const FfiResult result = call(amm_quote, input); + return result.ok ? result.value : publicError("backend_error"); +} + +LogosMap AmmModuleImpl::submitNewPosition(const LogosMap& request, + const std::string& quote_hash, + bool wallet_open, + const std::string& fresh_lp_id) { + if (m_requestPending) return publicError("submit_in_progress"); + if (!wallet_open) return publicError("wallet_unavailable"); + m_requestPending = true; + struct Guard { + bool* flag; + ~Guard() { *flag = false; } + } guard{&m_requestPending}; + + const Network net = network(); + json error; + const json input = buildQuoteInput(request, net, wallet_open, /*fresh=*/true, &error); + if (!error.is_null()) return error; + + const FfiResult quoteResult = call(amm_quote, input); + if (!quoteResult.ok) return publicError("backend_error"); + const json quote = quoteResult.value; + if (jStr(quote, "quoteHash") != quote_hash) { + json result = publicError("quote_changed"); + result["quote"] = quote; + return result; + } + if (!quote.value("canSubmit", false)) { + json result = publicError("quote_not_submittable"); + result["quote"] = quote; + return result; + } + + // Fresh LP holding: the app owns wallet-keyset mutation. If the quote needs + // one and the caller hasn't supplied it, ask for it (no submit) so the + // backend can create it through its own wallet provider and call again. + json freshLp; // null + if (quote.value("requiresFreshLp", false)) { + if (fresh_lp_id.empty()) { + return json{ + {"schema", SCHEMA}, + {"status", "requires_fresh_lp"}, + {"quote", quote}, + }; + } + freshLp = readPublicAccount(fresh_lp_id); + } + + json planInput = input; + planInput["quoteHash"] = quote_hash; + planInput["nowMs"] = nowMs(); + if (!freshLp.is_null()) planInput["freshLp"] = freshLp; + + const FfiResult planResult = call(amm_plan, planInput); + if (!planResult.ok) return publicError("backend_error"); + const json plan = planResult.value; + if (jStr(plan, "status") != "ready") { + const std::string code = jStr(plan, "code"); + return publicError(code.empty() ? "wallet_submission_failed" : code); + } + + uint64_t deadline = 0; + if (!parseU64(jStr(plan, "deadlineMs"), deadline) || nowMs() >= deadline) + return publicError("transaction_deadline_expired"); + + const std::vector accounts = jsonStrVec(plan.value("accountIds", json::array())); + const std::vector signers = jsonBoolVec(plan.value("signingRequirements", json::array())); + const std::vector instruction = jsonWordsToLeBytes(plan.value("instruction", json::array())); + const std::string program_id = jStr(plan, "programId"); + + const std::string reply = modules().logos_execution_zone.send_generic_public_transaction( + accounts, signers, instruction, program_id); + const auto obj = json::parse(reply, nullptr, /*allow_exceptions=*/false); + if (!obj.is_object() || !obj.value("success", false)) + return publicError("wallet_submission_failed"); + + // Native tx hash (64-char hex) -> base58 transaction id via the wallet + // module (avoids linking libbase58 just for this encode). + const std::string tx_hash = jStr(obj, "tx_hash"); + const std::string transaction_id = + modules().logos_execution_zone.account_id_to_base58(tx_hash); + if (transaction_id.empty()) return publicError("wallet_submission_failed"); + + return { + {"schema", SCHEMA}, + {"status", "submitted"}, + {"transactionId", transaction_id}, + {"deadlineMs", plan.value("deadlineMs", json())}, + }; +} diff --git a/modules/amm/src/amm_module_impl.h b/modules/amm/src/amm_module_impl.h new file mode 100644 index 00000000..4c2e307f --- /dev/null +++ b/modules/amm/src/amm_module_impl.h @@ -0,0 +1,146 @@ +#pragma once + +#include +#include +#include + +#include // LogosMap / LogosList (nlohmann::json aliases) +#include // LogosModuleContext base + modules() + +// AMM business logic as a universal core Logos module. +// +// Orchestration only: the AMM domain math (PDA derivation, on-chain account +// decoding, quote/plan computation, and instruction encoding) lives in the Rust +// `amm_client` crate and is reached through its JSON FFI (amm_client.h — one +// `char* op(const char*)` per operation, request and response both JSON). This +// module sequences those ops with chain I/O delegated to the +// `logos_execution_zone` wallet module (reached via modules().logos_execution_zone). +// +// The same surface is consumed by the QML UI (via modules().amm_module) and +// headlessly (logoscore call amm_module ...). Ported from the app-side +// SwapRuntime / NewPositionRuntime orchestration (apps/amm/src) plus the +// backend's network-context derivation, made Qt-free (std::string / LogosMap / +// LogosList / nlohmann::json) as the universal authoring model requires. +// +// Public methods ARE the module's API; the Qt plugin glue is generated from +// this header because metadata.json sets "interface": "universal". Keep the +// header Qt-free — std types only. +class AmmModuleImpl : public LogosModuleContext { +public: + AmmModuleImpl() = default; + ~AmmModuleImpl() = default; + + /// Derives the pool PDAs (config / pool / vaults / current-tick) for the + /// (def_a_hex, def_b_hex) pair and reads the pool's on-chain reserves. + /// Returns `{ "exists": false, "error": "" }` when the AMM program + /// bin isn't configured, the AMM isn't initialized, or the pool has no + /// liquidity yet; otherwise the resolved pool as a map (reserveA/reserveB in + /// the pool's canonical def order, feeBps, and the derived account ids). + LogosMap resolvePool(const std::string& def_a_hex, const std::string& def_b_hex); + + /// Submits an on-chain SwapExactInput transaction against the pool for + /// (def_a_hex = token in, def_b_hex = token out). amount_in / min_out are + /// u128 base-unit amounts; deadline is a u64 unix-ms timestamp. Each accepts + /// EITHER a small JSON integer (bare `1000` on the CLI) OR a decimal string + /// (what the UI passes, and what the CLI must use for any big value — large + /// amounts and the unix-ms deadline — as a quote-wrapped arg like + /// '"1000000000000000000"'). Declared `nlohmann::json` so the generated + /// dispatch hands us the raw value; JSON floats are rejected rather than + /// submit a silently-rounded amount. Returns the tx hash, or an empty string + /// on failure (no pool, unreadable AMM_PROGRAM_BIN, bad inputs, failed tx). + std::string swapExactInput(const std::string& def_a_hex, + const std::string& def_b_hex, + const std::string& user_input_holding_hex, + const std::string& user_output_holding_hex, + const nlohmann::json& amount_in, + const nlohmann::json& min_out, + const nlohmann::json& deadline); + + /// 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(); + + /// New-position (add-liquidity) view state: reads the AMM config + the + /// user's wallet accounts and returns the `new-position.v1` context map the + /// UI renders (available tokens, fee tiers, warnings). `wallet_open` gates + /// whether wallet accounts are included; `refresh_wallet_accounts` forces a + /// fresh read rather than a cached one. + LogosMap newPositionContext(const LogosMap& request, + bool wallet_open, + bool refresh_wallet_accounts); + + /// Prices an add-liquidity request against current on-chain state and + /// returns the `new-position.v1` quote map (quoteHash, canSubmit, + /// requiresFreshLp, amounts, warnings). Read-only — no submission. + LogosMap quoteNewPosition(const LogosMap& request, bool wallet_open); + + /// Submits an add-liquidity transaction. Re-quotes and validates `quote_hash` + /// against the current quote. If the quote requires a fresh LP holding and + /// `fresh_lp_id` is empty, returns `{ "status": "requires_fresh_lp", ... }` + /// WITHOUT submitting — the caller (the app backend, which owns the wallet + /// keyset) creates the account and calls again with its id. Otherwise builds + /// the plan (injecting the fresh LP account when given) and submits, then + /// returns the `new-position.v1` submitted/error map. + LogosMap submitNewPosition(const LogosMap& request, + const std::string& quote_hash, + bool wallet_open, + const std::string& fresh_lp_id); + +private: + // Off-chain "network" context, derived from the process env (the same + // sources the app backend used): AMM deployment id from AMM_PROGRAM_BIN, + // configured token set from TOKENS_CONFIG. `status` is "ready" once the + // program id resolves, else "config_missing" / "loading". + struct Network { + std::string id = "lez"; + std::string status; + std::string fingerprint; // == amm_program_id (binds a quote to the deploy) + std::string amm_program_id; // 64-char lowercase hex + std::vector token_ids; + }; + Network network(); + + // 64-char lowercase-hex AMM program id via the amm_client `program_id` op + // over the AMM_PROGRAM_BIN bytes (empty if unset/unreadable/bad). + std::string ammProgramId(); + + // Reads AMM_PROGRAM_BIN into a byte vector (empty on unset/unreadable/empty). + std::vector loadAmmElf(); + + // Normalizes an account id given as 64-char hex or base58 to lowercase hex + // (base58 via the wallet module). Empty string if it is neither. + std::string normalizeAccountId(const std::string& id); + + // Derives the config account id (amm_config_id) and reads it, returning the + // account-read shape the amm_client ops embed. Null json when the config_id + // op itself fails (readPublicAccount always yields at least {id,status}). + nlohmann::json readConfig(const Network& net); + + // Reads a public account through the wallet module and returns the + // { id, status, account:{ program_owner, balance, nonce, data } } shape the + // amm_client ops expect (see the app-side accountReadJson). `account` is + // omitted when the read has no data (uninitialized/nonexistent). + nlohmann::json readPublicAccount(const std::string& account_id); + + // The user's own public accounts as an array of readPublicAccount() shapes + // (empty when the wallet is closed). `refresh` is advisory — the wallet + // module owns its own sync. + nlohmann::json walletAccountReads(bool wallet_open, bool refresh); + + // Builds the { networkId, networkFingerprint, ammProgramId, request, + // snapshot } input shared by quoteNewPosition / submitNewPosition. On a + // recoverable precondition failure, sets *error to a new-position.v1 error + // map and returns a null json. + nlohmann::json buildQuoteInput(const LogosMap& request, + const Network& net, + bool wallet_open, + bool fresh_wallet_accounts, + nlohmann::json* error); + + // Guards against a re-entrant in-flight request (e.g. a double-submit) on the + // shared module instance. Released per call, so the app's fresh-LP resubmit + // still proceeds. + bool m_requestPending = false; +}; From d5dcbf40e0016f6612c563550444af9aff895024 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:57:24 +0200 Subject: [PATCH 2/2] refactor(apps/amm): move the amm_client crate into modules/amm/ffi as amm_ffi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the amm_module refactor, this crate is linked only by the module (the UI delegates to modules().amm_module and links nothing), so its home under apps/amm/ and the name "client" were both misnomers: it's the AMM business logic the module wraps, reached across an FFI boundary — the same relationship logos_execution_zone has with wallet_ffi. Co-locate it with the module that owns it and name it for that role. The FFI surface is unchanged — the exported functions are already amm_* (not amm_client_*) — so only the crate, directory, generated header, dylib, and package names move. The module's call sites are untouched; it just includes the renamed header. - apps/amm/client → modules/amm/ffi (git-tracked rename; history preserved) - crate amm_client → amm_ffi: package name, include/amm_ffi.h, libamm_ffi.dylib, AMM_FFI_H guard, build.rs output path, tests/public_api.rs import - workspace member path + Cargo.lock - flake.nix: pname, -p, header-copy path, dylib install_name, packages.amm_ffi, ammModuleOutputs externalLibInputs, DYLD wrapper - modules/amm: metadata external_libraries, CMakeLists EXTERNAL_LIBS, impl #include + comments, README, flake note - apps/amm/flake.nix: drop the now-dead amm_client external-lib input (the UI links no external lib of its own) Resulting layout: modules/amm/ src/ # C++ module (amm_module_impl.{h,cpp}) ffi/ # Rust crate amm_ffi (Cargo.toml, src/, include/amm_ffi.h) --- Cargo.lock | 26 ++++++------ Cargo.toml | 2 +- apps/amm/CMakeLists.txt | 2 +- apps/amm/README.md | 2 +- apps/amm/VALIDATION.md | 4 +- apps/amm/flake.nix | 27 ++++-------- apps/amm/src/AmmUiBackend.h | 2 +- flake.nix | 42 +++++++++---------- modules/amm/CMakeLists.txt | 6 +-- modules/amm/README.md | 16 +++---- .../amm/client => modules/amm/ffi}/Cargo.toml | 2 +- {apps/amm/client => modules/amm/ffi}/build.rs | 2 +- .../client => modules/amm/ffi}/cbindgen.toml | 2 +- .../amm/ffi/include/amm_ffi.h | 6 +-- .../client => modules/amm/ffi}/src/account.rs | 0 .../amm/ffi}/src/api/accounts.rs | 0 .../amm/ffi}/src/api/clock.rs | 0 .../amm/ffi}/src/api/commitment.rs | 0 .../amm/ffi}/src/api/config.rs | 0 .../amm/ffi}/src/api/context.rs | 0 .../amm/ffi}/src/api/funding.rs | 0 .../amm/ffi}/src/api/holding.rs | 0 .../client => modules/amm/ffi}/src/api/mod.rs | 0 .../amm/ffi}/src/api/pair.rs | 0 .../amm/ffi}/src/api/plan.rs | 0 .../amm/ffi}/src/api/position.rs | 0 .../amm/ffi}/src/api/quote.rs | 0 .../amm/ffi}/src/api/quote_error.rs | 0 .../amm/ffi}/src/api/request.rs | 0 .../amm/ffi}/src/api/swap.rs | 0 .../amm/ffi}/src/api/tests.rs | 0 .../amm/client => modules/amm/ffi}/src/ffi.rs | 0 .../amm/client => modules/amm/ffi}/src/lib.rs | 0 .../amm/ffi}/tests/public_api.rs | 2 +- modules/amm/flake.nix | 4 +- modules/amm/metadata.json | 2 +- modules/amm/src/amm_module_impl.cpp | 16 +++---- modules/amm/src/amm_module_impl.h | 8 ++-- 38 files changed, 81 insertions(+), 92 deletions(-) rename {apps/amm/client => modules/amm/ffi}/Cargo.toml (97%) rename {apps/amm/client => modules/amm/ffi}/build.rs (80%) rename {apps/amm/client => modules/amm/ffi}/cbindgen.toml (81%) rename apps/amm/client/include/amm_client.h => modules/amm/ffi/include/amm_ffi.h (93%) rename {apps/amm/client => modules/amm/ffi}/src/account.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/accounts.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/clock.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/commitment.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/config.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/context.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/funding.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/holding.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/mod.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/pair.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/plan.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/position.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/quote.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/quote_error.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/request.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/swap.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/api/tests.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/ffi.rs (100%) rename {apps/amm/client => modules/amm/ffi}/src/lib.rs (100%) rename {apps/amm/client => modules/amm/ffi}/tests/public_api.rs (86%) diff --git a/Cargo.lock b/Cargo.lock index 0147201a..33bb2f9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,38 +77,38 @@ dependencies = [ ] [[package]] -name = "amm_client" +name = "amm_core" version = "0.1.0" dependencies = [ "alloy-primitives", - "amm_core", "borsh", - "cbindgen", - "clock_core", - "hex", "lee_core", - "pretty_assertions", - "risc0-binfmt", "risc0-zkvm", + "ruint", "serde", - "serde_json", - "sha2", + "spel-framework-macros", "token_core", - "twap_oracle_core", ] [[package]] -name = "amm_core" +name = "amm_ffi" version = "0.1.0" dependencies = [ "alloy-primitives", + "amm_core", "borsh", + "cbindgen", + "clock_core", + "hex", "lee_core", + "pretty_assertions", + "risc0-binfmt", "risc0-zkvm", - "ruint", "serde", - "spel-framework-macros", + "serde_json", + "sha2", "token_core", + "twap_oracle_core", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 35a1dee8..76b127fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = [ - "apps/amm/client", + "modules/amm/ffi", "programs/token/core", "programs/token", "programs/token/methods", diff --git a/apps/amm/CMakeLists.txt b/apps/amm/CMakeLists.txt index 0d075360..5ad917a8 100644 --- a/apps/amm/CMakeLists.txt +++ b/apps/amm/CMakeLists.txt @@ -26,7 +26,7 @@ add_subdirectory("${LOGOS_WALLET_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/share # generated *SimpleSource/*ViewPluginBase). Mirrors the LEZ wallet UI module. # The AMM business logic lives in the amm_module core module (declared as a # dependency in metadata.json, reached via modules().amm_module in the backend), -# so the UI links no amm_client library of its own. +# so the UI links no amm_ffi library of its own. logos_module( NAME amm_ui REP_FILE src/AmmUiBackend.rep diff --git a/apps/amm/README.md b/apps/amm/README.md index 5a4f4eee..6c7e4f06 100644 --- a/apps/amm/README.md +++ b/apps/amm/README.md @@ -63,7 +63,7 @@ nix run .#amm-ui This builds and runs the application in development mode. The Logos bridge is unavailable in standalone mode, but the UI layout and mock data are fully functional. Build just the AMM core module with `nix build .#amm-module`, or its underlying -client crate with `nix build .#amm_client`. (Each UI is exposed under its own +logic crate with `nix build .#amm_ffi`. (Each UI is exposed under its own name, so future apps are `nix run .#` — there is no bare `nix run .` default.) diff --git a/apps/amm/VALIDATION.md b/apps/amm/VALIDATION.md index 8663bfa5..89216fb4 100644 --- a/apps/amm/VALIDATION.md +++ b/apps/amm/VALIDATION.md @@ -8,8 +8,8 @@ syntax, and the complete module build. Run from the repository root: ```bash -cargo +1.94.0 test -p amm_client -cargo +1.94.0 clippy -p amm_client --all-targets -- -D warnings +cargo +1.94.0 test -p amm_ffi +cargo +1.94.0 clippy -p amm_ffi --all-targets -- -D warnings logos_qml=$(nix build github:logos-co/logos-design-system/6176f0d7a5dfeb64a7f0f98e7ca2bf71a4804772 --no-link --print-out-paths) amm_qml=$(nix build ./apps/amm#packages.x86_64-linux.default --no-link --print-out-paths) qt_qml=$(nix-store --query --requisites "$amm_qml" | rg -m1 -- '-qtdeclarative-[0-9]') diff --git a/apps/amm/flake.nix b/apps/amm/flake.nix index 6b784955..63196943 100644 --- a/apps/amm/flake.nix +++ b/apps/amm/flake.nix @@ -26,21 +26,15 @@ inputs.logos-execution-zone.url = "github:logos-blockchain/logos-execution-zone?rev=a7e06a660940a00093b1760560d37ff84aff5a05"; }; - - amm_client.url = "path:../.."; }; - # NOTE: this flake is no longer built standalone. The amm_client crate - # (the Rust C FFI library the AmmUiBackend C++ code links against) lives in - # the repo-root flake, and referencing it from here would require either a - # hardcoded absolute `git+file://` path or a `path:../..` input — the latter - # fails flake evaluation because the app directory is copied into the Nix - # store as its own flake root, so `../..` can't escape it there. Instead, - # the repo-root flake.nix builds this module directly (src = ./apps/amm) - # and resolves amm_client via `self`. The repo-root flake exposes the UI - # as a named attribute (there is no bare `default`): run it from the repo root - # with `nix run .#amm-ui`, and build just the FFI crate with - # `nix build .#amm_client`. + # NOTE: this flake is no longer built standalone; the repo-root flake.nix + # builds the UI directly (src = ./apps/amm). The UI links no external lib of + # its own — the AMM logic lives in the amm_ffi crate, which the amm_module + # core module links; the UI reaches it via modules().amm_module (declared in + # metadata.json `dependencies`). The repo-root flake exposes the UI as a named + # attribute (there is no bare `default`): run it with `nix run .#amm-ui`, and + # build the AMM logic crate with `nix build .#amm_ffi`. outputs = inputs@{ logos-module-builder, shared_wallet, ... }: logos-module-builder.lib.mkLogosQmlModule { src = ./.; @@ -49,12 +43,7 @@ preConfigure = '' cmakeFlagsArray+=("-DLOGOS_WALLET_SOURCE_DIR=${shared_wallet}") ''; - externalLibInputs = { - amm_client = { - input = inputs.amm_client; - packages.default = "amm_client"; - }; - }; + externalLibInputs = { }; postInstall = '' # The builder installs the view under lib/qml after this hook. Its # import descriptor points back to this compiled shared QML module. diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index f90d4ef1..3cda4eba 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -74,7 +74,7 @@ public slots: LogosAPI* m_logosAPI; // Handle for the amm_module core module (resolvePool / swapExactInput / - // tokenList / new-position). The module wraps the amm_client brain and + // tokenList / new-position). 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/flake.nix b/flake.nix index 16d39651..1adcc932 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ }; # The AMM QML UI module (apps/amm) is built from this same flake so it can - # reference the amm_client crate package via `self` — no filesystem + # reference the amm_ffi crate package via `self` — no filesystem # path or git-remote reference to this repo is needed (see apps/amm/flake.nix # history: a `git+file://` URL pointing at a local checkout is # machine-specific and not portable). @@ -54,19 +54,19 @@ craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; # Whole workspace: crane needs Cargo.lock + all path deps (amm_core, - # twap_oracle_core, token_core, ...) to resolve `-p amm_client`. + # twap_oracle_core, token_core, ...) to resolve `-p amm_ffi`. src = ./.; commonArgs = { inherit src; strictDeps = true; - pname = "amm_client"; + pname = "amm_ffi"; version = "0.1.0"; # CRITICAL: scope to ONLY this crate. The workspace also contains # `amm/methods` etc. whose build.rs compiles the risc0 guest (which - # WOULD invoke Metal on darwin). `-p amm_client` never builds + # WOULD invoke Metal on darwin). `-p amm_ffi` never builds # those crates or their build scripts. - cargoExtraArgs = "-p amm_client"; + cargoExtraArgs = "-p amm_ffi"; doCheck = false; # NOTE: cbindgen is used here as a Cargo *build-dependency* # (invoked from build.rs via its Rust library API), not as the @@ -76,17 +76,17 @@ cargoArtifacts = craneLib.buildDepsOnly commonArgs; - # The single AMM host FFI crate (apps/amm/client): swap + new-position + # The single AMM host FFI crate (modules/amm/ffi): swap + new-position # operations behind one JSON C ABI. Its header is cbindgen-generated - # into include/amm_client.h at build time. - ammClient = craneLib.buildPackage ( + # into include/amm_ffi.h at build time. + ammFfi = craneLib.buildPackage ( commonArgs // { inherit cargoArtifacts; postInstall = '' mkdir -p $out/include - cp apps/amm/client/include/amm_client.h $out/include/ + cp modules/amm/ffi/include/amm_ffi.h $out/include/ '' + pkgs.lib.optionalString pkgs.stdenv.isDarwin '' # Set the dylib's install-name to its ABSOLUTE store path (NOT @@ -95,20 +95,20 @@ # @rpath id fails to dlopen at launch. An absolute /nix/store id # is recorded in the plugin's LC_LOAD_DYLIB, kept in the closure # by Nix, and resolved directly at runtime — no rpath needed. - if [ -f $out/lib/libamm_client.dylib ]; then - install_name_tool -id "$out/lib/libamm_client.dylib" $out/lib/libamm_client.dylib + if [ -f $out/lib/libamm_ffi.dylib ]; then + install_name_tool -id "$out/lib/libamm_ffi.dylib" $out/lib/libamm_ffi.dylib fi ''; } ); in { - packages.default = ammClient; - packages.amm_client = ammClient; + packages.default = ammFfi; + packages.amm_ffi = ammFfi; } ); - # The AMM QML UI module (apps/amm). It links no amm_client library of its + # The AMM QML UI module (apps/amm). It links no amm_ffi library of its # own — the AMM logic lives in the amm_module core module, which the UI # depends on (declared in apps/amm/metadata.json, reached via # modules().amm_module in the backend) alongside the logos_execution_zone @@ -123,7 +123,7 @@ # direct dep too, so both the UI and amm_module resolve the one shared # wallet instance. flakeInputs = inputs // { amm_module = ammModuleOutputs; }; - # The UI links no external lib of its own — the AMM brain (amm_client) is + # The UI links no external lib of its own — the AMM brain (amm_ffi) is # linked by amm_module, which the UI reaches via modules().amm_module. externalLibInputs = { }; # The AMM UI links the shared C++ wallet access lib and bundles the @@ -159,7 +159,7 @@ appPkgs = appOutputs.packages or { }; # AMM core module (modules/amm): the AMM business logic as a headless - # `core` Logos module. It links the amm_client crate (the transport- + # `core` Logos module. It links the amm_ffi crate (the transport- # independent AMM brain, resolved via `self`) and depends on the # logos_execution_zone wallet module (declared in modules/amm/metadata.json, # reached via modules().logos_execution_zone in the impl). Exposed as the @@ -169,25 +169,25 @@ configFile = ./modules/amm/metadata.json; flakeInputs = inputs; externalLibInputs = { - amm_client = { input = self; packages.default = "amm_client"; }; + amm_ffi = { input = self; packages.default = "amm_ffi"; }; }; }; ammModulePkgs = ammModuleOutputs.packages or { }; # Wrap the app launcher to export DYLD_FALLBACK_LIBRARY_PATH pointing at the - # amm_client lib. The logos module builder links the plugin against - # @rpath/libamm_client.dylib but does NOT stage that dylib into the + # amm_ffi lib. The logos module builder links the plugin against + # @rpath/libamm_ffi.dylib but does NOT stage that dylib into the # plugin-dir it loads at runtime, so dlopen fails with "Failed to load UI # plugin". Adding the crate's store lib dir to DYLD's fallback search path # lets the loader find it (the store path stays in the closure). wrapWithDyld = system: app: let pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlays.default ]; }; - ammClient = crateOutputs.packages.${system}.amm_client; + ammFfi = crateOutputs.packages.${system}.amm_ffi; in app // { program = "${pkgs.writeShellScript "run-amm-ui" '' - export DYLD_FALLBACK_LIBRARY_PATH="${ammClient}/lib''${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" + export DYLD_FALLBACK_LIBRARY_PATH="${ammFfi}/lib''${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" exec ${app.program} "$@" ''}"; }; diff --git a/modules/amm/CMakeLists.txt b/modules/amm/CMakeLists.txt index 817ab566..3c234946 100644 --- a/modules/amm/CMakeLists.txt +++ b/modules/amm/CMakeLists.txt @@ -16,9 +16,9 @@ string(JSON MODULE_NAME GET ${METADATA_JSON} name) # Universal core module: we write only the impl class; the Qt plugin glue is # generated from src/amm_module_impl.h (because metadata.json sets -# "interface": "universal"). EXTERNAL_LIBS links amm_client — the pure-Rust, +# "interface": "universal"). EXTERNAL_LIBS links amm_ffi — the pure-Rust, # transport-independent AMM brain (PDA/decode/quote/plan/encode) exposed as a -# JSON FFI (amm_client.h), the same library the UI links. The chain I/O +# JSON FFI (amm_ffi.h), the same library the UI links. The chain I/O # dependency (logos_execution_zone) is declared in metadata.json and reached via # modules().logos_execution_zone in the impl, so it is NOT listed here. logos_module( @@ -27,5 +27,5 @@ logos_module( src/amm_module_impl.h src/amm_module_impl.cpp EXTERNAL_LIBS - amm_client + amm_ffi ) diff --git a/modules/amm/README.md b/modules/amm/README.md index d42d8ac5..5ce4142e 100644 --- a/modules/amm/README.md +++ b/modules/amm/README.md @@ -11,7 +11,7 @@ for the module framework. ## What it does The impl class `AmmModuleImpl` (`src/amm_module_impl.{h,cpp}`) is a **transport -adapter**: the AMM domain math lives in the Rust `amm_client` crate (a +adapter**: the AMM domain math lives in the Rust `amm_ffi` crate (a transport-independent JSON FFI), and this module sequences those pure ops with chain I/O delegated to the `logos_execution_zone` wallet module. Its public methods (the module API is generated from the header) are: @@ -41,7 +41,7 @@ methods (the module API is generated from the header) are: ## How it fits together ``` -QML ──modules().amm_module──┐ ┌── amm_client (Rust cdylib, JSON FFI): +QML ──modules().amm_module──┐ ┌── amm_ffi (Rust cdylib, JSON FFI): CLI ──logoscore call────────┤ │ PDA derivation, account decode, ▼ │ quote/plan math, instruction encoding amm_module ───┤ — transport-independent (external_libraries) @@ -51,7 +51,7 @@ CLI ──logoscore call────────┤ │ PDA derivation via modules().logos_execution_zone.* ``` -The `amm_client` crate is deliberately I/O-free — each op takes the account data +The `amm_ffi` crate is deliberately I/O-free — each op takes the account data it needs as JSON input and returns a JSON result. This module is the transport adapter the crate is designed to require: it fetches accounts through the wallet module (`get_account_public`, `list_accounts`), hands them to the pure Rust op, @@ -86,12 +86,12 @@ The UI passes `QString` (→ `QVariant` → string branch) and is unaffected. ## Build -Built from the **repo-root** flake (which provides the `amm_client` library it +Built from the **repo-root** flake (which provides the `amm_ffi` library it links): ```bash nix build .#amm-module -# output: result/lib/amm_module_plugin.dylib (+ libamm_client.dylib) +# output: result/lib/amm_module_plugin.dylib (+ libamm_ffi.dylib) ``` ## Runtime configuration @@ -123,7 +123,7 @@ Have all of the following in place before staging the modules dir: `lm` introspects a built plugin without running it — handy to confirm the API (`lm result/lib/amm_module_plugin.dylib` shows methods, signatures, deps). -3. **This module, built** (produces `amm_module_plugin.dylib` + `libamm_client.dylib`): +3. **This module, built** (produces `amm_module_plugin.dylib` + `libamm_ffi.dylib`): ```bash nix build .#amm-module # from the repo root; output under result/lib/ @@ -165,7 +165,7 @@ verify the manifest hashes at load time. modules/ amm_module/ amm_module_plugin.dylib - libamm_client.dylib + libamm_ffi.dylib variant # one line: darwin-arm64-dev manifest.json logos_execution_zone/ @@ -255,6 +255,6 @@ the fork pinned as the `logos_execution_zone` input. See - **`swapExactOutput` is not exposed yet.** The on-chain program supports it (`amm_core::Instruction::SwapExactOutput`, identical account layout to `SwapExactInput`), but the client path was only ever built for exact-input: - `amm_client` has no exact-output op and neither the UI nor this module has a + `amm_ffi` has no exact-output op and neither the UI nor this module has a `swapExactOutput` method. Adding it is a near-copy of the exact-input path — an `amm_swap_exact_output_*` op in the crate plus a `swapExactOutput` method here. diff --git a/apps/amm/client/Cargo.toml b/modules/amm/ffi/Cargo.toml similarity index 97% rename from apps/amm/client/Cargo.toml rename to modules/amm/ffi/Cargo.toml index d3e2be76..17392867 100644 --- a/apps/amm/client/Cargo.toml +++ b/modules/amm/ffi/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "amm_client" +name = "amm_ffi" version = "0.1.0" edition = "2021" diff --git a/apps/amm/client/build.rs b/modules/amm/ffi/build.rs similarity index 80% rename from apps/amm/client/build.rs rename to modules/amm/ffi/build.rs index 0ca66455..c6a14361 100644 --- a/apps/amm/client/build.rs +++ b/modules/amm/ffi/build.rs @@ -3,7 +3,7 @@ fn main() { std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo"); cbindgen::generate(&crate_dir) .expect("cbindgen") - .write_to_file(format!("{crate_dir}/include/amm_client.h")); + .write_to_file(format!("{crate_dir}/include/amm_ffi.h")); println!("cargo:rerun-if-changed=src"); println!("cargo:rerun-if-changed=cbindgen.toml"); } diff --git a/apps/amm/client/cbindgen.toml b/modules/amm/ffi/cbindgen.toml similarity index 81% rename from apps/amm/client/cbindgen.toml rename to modules/amm/ffi/cbindgen.toml index 210f8e52..2b4ca030 100644 --- a/apps/amm/client/cbindgen.toml +++ b/modules/amm/ffi/cbindgen.toml @@ -1,5 +1,5 @@ language = "C" -include_guard = "AMM_CLIENT_H" +include_guard = "AMM_FFI_H" pragma_once = true cpp_compat = true autogen_warning = "/* Generated by cbindgen. Do not edit. */" diff --git a/apps/amm/client/include/amm_client.h b/modules/amm/ffi/include/amm_ffi.h similarity index 93% rename from apps/amm/client/include/amm_client.h rename to modules/amm/ffi/include/amm_ffi.h index 5b2eb025..69684a64 100644 --- a/apps/amm/client/include/amm_client.h +++ b/modules/amm/ffi/include/amm_ffi.h @@ -1,5 +1,5 @@ -#ifndef AMM_CLIENT_H -#define AMM_CLIENT_H +#ifndef AMM_FFI_H +#define AMM_FFI_H #pragma once @@ -46,4 +46,4 @@ void amm_free(char *value); } // extern "C" #endif // __cplusplus -#endif /* AMM_CLIENT_H */ +#endif /* AMM_FFI_H */ diff --git a/apps/amm/client/src/account.rs b/modules/amm/ffi/src/account.rs similarity index 100% rename from apps/amm/client/src/account.rs rename to modules/amm/ffi/src/account.rs diff --git a/apps/amm/client/src/api/accounts.rs b/modules/amm/ffi/src/api/accounts.rs similarity index 100% rename from apps/amm/client/src/api/accounts.rs rename to modules/amm/ffi/src/api/accounts.rs diff --git a/apps/amm/client/src/api/clock.rs b/modules/amm/ffi/src/api/clock.rs similarity index 100% rename from apps/amm/client/src/api/clock.rs rename to modules/amm/ffi/src/api/clock.rs diff --git a/apps/amm/client/src/api/commitment.rs b/modules/amm/ffi/src/api/commitment.rs similarity index 100% rename from apps/amm/client/src/api/commitment.rs rename to modules/amm/ffi/src/api/commitment.rs diff --git a/apps/amm/client/src/api/config.rs b/modules/amm/ffi/src/api/config.rs similarity index 100% rename from apps/amm/client/src/api/config.rs rename to modules/amm/ffi/src/api/config.rs diff --git a/apps/amm/client/src/api/context.rs b/modules/amm/ffi/src/api/context.rs similarity index 100% rename from apps/amm/client/src/api/context.rs rename to modules/amm/ffi/src/api/context.rs diff --git a/apps/amm/client/src/api/funding.rs b/modules/amm/ffi/src/api/funding.rs similarity index 100% rename from apps/amm/client/src/api/funding.rs rename to modules/amm/ffi/src/api/funding.rs diff --git a/apps/amm/client/src/api/holding.rs b/modules/amm/ffi/src/api/holding.rs similarity index 100% rename from apps/amm/client/src/api/holding.rs rename to modules/amm/ffi/src/api/holding.rs diff --git a/apps/amm/client/src/api/mod.rs b/modules/amm/ffi/src/api/mod.rs similarity index 100% rename from apps/amm/client/src/api/mod.rs rename to modules/amm/ffi/src/api/mod.rs diff --git a/apps/amm/client/src/api/pair.rs b/modules/amm/ffi/src/api/pair.rs similarity index 100% rename from apps/amm/client/src/api/pair.rs rename to modules/amm/ffi/src/api/pair.rs diff --git a/apps/amm/client/src/api/plan.rs b/modules/amm/ffi/src/api/plan.rs similarity index 100% rename from apps/amm/client/src/api/plan.rs rename to modules/amm/ffi/src/api/plan.rs diff --git a/apps/amm/client/src/api/position.rs b/modules/amm/ffi/src/api/position.rs similarity index 100% rename from apps/amm/client/src/api/position.rs rename to modules/amm/ffi/src/api/position.rs diff --git a/apps/amm/client/src/api/quote.rs b/modules/amm/ffi/src/api/quote.rs similarity index 100% rename from apps/amm/client/src/api/quote.rs rename to modules/amm/ffi/src/api/quote.rs diff --git a/apps/amm/client/src/api/quote_error.rs b/modules/amm/ffi/src/api/quote_error.rs similarity index 100% rename from apps/amm/client/src/api/quote_error.rs rename to modules/amm/ffi/src/api/quote_error.rs diff --git a/apps/amm/client/src/api/request.rs b/modules/amm/ffi/src/api/request.rs similarity index 100% rename from apps/amm/client/src/api/request.rs rename to modules/amm/ffi/src/api/request.rs diff --git a/apps/amm/client/src/api/swap.rs b/modules/amm/ffi/src/api/swap.rs similarity index 100% rename from apps/amm/client/src/api/swap.rs rename to modules/amm/ffi/src/api/swap.rs diff --git a/apps/amm/client/src/api/tests.rs b/modules/amm/ffi/src/api/tests.rs similarity index 100% rename from apps/amm/client/src/api/tests.rs rename to modules/amm/ffi/src/api/tests.rs diff --git a/apps/amm/client/src/ffi.rs b/modules/amm/ffi/src/ffi.rs similarity index 100% rename from apps/amm/client/src/ffi.rs rename to modules/amm/ffi/src/ffi.rs diff --git a/apps/amm/client/src/lib.rs b/modules/amm/ffi/src/lib.rs similarity index 100% rename from apps/amm/client/src/lib.rs rename to modules/amm/ffi/src/lib.rs diff --git a/apps/amm/client/tests/public_api.rs b/modules/amm/ffi/tests/public_api.rs similarity index 86% rename from apps/amm/client/tests/public_api.rs rename to modules/amm/ffi/tests/public_api.rs index 7d38289d..e5ab2798 100644 --- a/apps/amm/client/tests/public_api.rs +++ b/modules/amm/ffi/tests/public_api.rs @@ -1,4 +1,4 @@ -use amm_client::{config_id, ConfigIdRequest, NEW_POSITION_SCHEMA}; +use amm_ffi::{config_id, ConfigIdRequest, NEW_POSITION_SCHEMA}; #[test] fn direct_rust_api_does_not_require_ffi() { diff --git a/modules/amm/flake.nix b/modules/amm/flake.nix index 2a588138..dc3c7cd8 100644 --- a/modules/amm/flake.nix +++ b/modules/amm/flake.nix @@ -11,13 +11,13 @@ logos_execution_zone.url = "github:gravityblast/logos-execution-zone-module?ref=fix/generic-tx-instruction-bstr"; }; - # NOTE: like apps/amm, this flake is NOT built standalone. The amm_client + # NOTE: like apps/amm, this flake is NOT built standalone. The amm_ffi # crate this module links (the Rust JSON-FFI brain) lives in the repo-root # flake, and referencing it from here would require a hardcoded `git+file://` # path or a `path:../..` input — the latter fails flake evaluation because this # dir is copied into the Nix store as its own flake root, so `../..` can't # escape it. Instead, the repo-root flake.nix builds this module directly - # (src = ./modules/amm) and resolves amm_client via `self`. Build it from + # (src = ./modules/amm) and resolves amm_ffi via `self`. Build it from # the repo root: # nix build .#amm-module outputs = inputs@{ logos-module-builder, ... }: diff --git a/modules/amm/metadata.json b/modules/amm/metadata.json index 90096063..827e6c5d 100644 --- a/modules/amm/metadata.json +++ b/modules/amm/metadata.json @@ -14,7 +14,7 @@ "runtime": [] }, "external_libraries": [ - { "name": "amm_client" } + { "name": "amm_ffi" } ], "cmake": { "find_packages": [], diff --git a/modules/amm/src/amm_module_impl.cpp b/modules/amm/src/amm_module_impl.cpp index 6dbda1be..dff3b7e7 100644 --- a/modules/amm/src/amm_module_impl.cpp +++ b/modules/amm/src/amm_module_impl.cpp @@ -20,7 +20,7 @@ #include "logos_sdk.h" extern "C" { -#include "amm_client.h" +#include "amm_ffi.h" } namespace { @@ -197,20 +197,20 @@ std::vector jsonWordsToLeBytes(const json& arr) { return out; } -// Result of an amm_client JSON op: the `{ ok, value }` envelope decoded. +// Result of an amm_ffi JSON op: the `{ ok, value }` envelope decoded. struct FfiResult { bool ok = false; json value; }; -// Serialize `request`, hand it to an amm_client op, and decode its +// Serialize `request`, hand it to an amm_ffi op, and decode its // `{ ok, value, error }` envelope. Mirrors apps/amm/src/AmmClient.cpp. `value` // is only populated (and `ok` true) when the op reports success with an object. FfiResult call(char* (*op)(const char*), const json& request) { const std::string payload = request.dump(); char* raw = op(payload.c_str()); if (raw == nullptr) { - AMM_TRACE("amm_client op returned null"); + AMM_TRACE("amm_ffi op returned null"); return {}; } const std::string response(raw); @@ -218,16 +218,16 @@ FfiResult call(char* (*op)(const char*), const json& request) { const auto doc = json::parse(response, nullptr, /*allow_exceptions=*/false); if (!doc.is_object()) { - AMM_TRACE("amm_client op returned invalid JSON"); + AMM_TRACE("amm_ffi op returned invalid JSON"); return {}; } if (!doc.value("ok", false)) { - AMM_TRACE("amm_client op failure: " << doc.value("error", std::string())); + AMM_TRACE("amm_ffi op failure: " << doc.value("error", std::string())); return {}; } const auto it = doc.find("value"); if (it == doc.end() || !it->is_object()) { - AMM_TRACE("amm_client op value is not an object"); + AMM_TRACE("amm_ffi op value is not an object"); return {}; } return {true, *it}; @@ -300,7 +300,7 @@ std::vector AmmModuleImpl::loadAmmElf() { std::string AmmModuleImpl::ammProgramId() { const std::vector elf = loadAmmElf(); if (elf.empty()) return {}; - // Hand the deployed binary to the amm_client program_id op, which decodes it + // Hand the deployed binary to the amm_ffi program_id op, which decodes it // and computes the Image ID — 64-char lowercase hex, little-endian per u32 // word (matches `spel program-id` and the on-chain *_program_id fields). const FfiResult r = call(amm_program_id, json{{"elf", toHex(elf.data(), elf.size())}}); diff --git a/modules/amm/src/amm_module_impl.h b/modules/amm/src/amm_module_impl.h index 4c2e307f..440bc8c8 100644 --- a/modules/amm/src/amm_module_impl.h +++ b/modules/amm/src/amm_module_impl.h @@ -11,7 +11,7 @@ // // Orchestration only: the AMM domain math (PDA derivation, on-chain account // decoding, quote/plan computation, and instruction encoding) lives in the Rust -// `amm_client` crate and is reached through its JSON FFI (amm_client.h — one +// `amm_ffi` crate and is reached through its JSON FFI (amm_ffi.h — one // `char* op(const char*)` per operation, request and response both JSON). This // module sequences those ops with chain I/O delegated to the // `logos_execution_zone` wallet module (reached via modules().logos_execution_zone). @@ -102,7 +102,7 @@ class AmmModuleImpl : public LogosModuleContext { }; Network network(); - // 64-char lowercase-hex AMM program id via the amm_client `program_id` op + // 64-char lowercase-hex AMM program id via the amm_ffi `program_id` op // over the AMM_PROGRAM_BIN bytes (empty if unset/unreadable/bad). std::string ammProgramId(); @@ -114,13 +114,13 @@ class AmmModuleImpl : public LogosModuleContext { std::string normalizeAccountId(const std::string& id); // Derives the config account id (amm_config_id) and reads it, returning the - // account-read shape the amm_client ops embed. Null json when the config_id + // account-read shape the amm_ffi ops embed. Null json when the config_id // op itself fails (readPublicAccount always yields at least {id,status}). nlohmann::json readConfig(const Network& net); // Reads a public account through the wallet module and returns the // { id, status, account:{ program_owner, balance, nonce, data } } shape the - // amm_client ops expect (see the app-side accountReadJson). `account` is + // amm_ffi ops expect (see the app-side accountReadJson). `account` is // omitted when the read has no data (uninitialized/nonexistent). nlohmann::json readPublicAccount(const std::string& account_id);