# 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_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: - `resolvePoolAccount(defAHex, defBHex)` — derives the pool PDA and reads/decodes the pool account (reserves in canonical `a`/`b` order, fee tier). On success `{ status: "ok", error: "", poolId, defAHex, defBHex, vaultAId, vaultBId, lpDefinitionId, reserveA, reserveB, liquiditySupply, feeBps }`; an absent / uninitialized pool or one with no liquidity is `{ status: "error", error: "no_pool", poolId }` (other codes: `no_program_bin`, `amm_not_initialized`, `bad_config`). - `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. - `resolveTokens(request, walletOpen)` — resolves an app-provided set of token ids into selector rows (definition + wallet holding per id). The lean, stateless successor to the removed `newPositionContext` path: the app owns the id set, so there is no network envelope or process-cached wallet state here. - `feeTiers()` — the AMM's supported fee tiers as raw basis points. - `createPoolQuote(request)` / `createPool(request)` and `addLiquidityQuote(request)` / `addLiquidity(request)` — the add-liquidity preview (read-only) and submit paths. The submit forwards the app-supplied fresh LP holding id; the app backend, which owns the wallet keyset, creates that account. ## How it fits together ``` 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) │ │ │ └── logos_execution_zone (dependency): ▼ chain reads + tx submit + base58, via modules().logos_execution_zone.* ``` 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, 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 accept base58 or hex.** The `*Hex` args and request-map ids are normalized at each method's boundary (via `logos_execution_zone.account_id_from_base58` for base58 inputs), so the wallet/runbook's base58 ids can be passed directly. **Amounts (`amountIn`/`minOut`, u128) and `deadline` (u64 unix-ms)** are declared `nlohmann::json`, so each accepts **either a JSON number or a decimal string**: - **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_ffi` library it links): ```bash nix build .#amm-module # output: result/lib/amm_module_plugin.dylib (+ libamm_ffi.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, `resolvePoolAccount` returns `{ status: "error", error: "no_program_bin" }`. (The token list config `TOKENS_CONFIG` is an **app** concern now — the module no longer reads it; see `apps/amm/README.md`.) ## Headless usage with `logoscore` ### 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_ffi.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 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. Every op (including `resolvePoolAccount`) reads on-chain through the wallet module's `get_account_public`, which needs the wallet **open** (the handle is null until `open`/`create_new`); `swapExactInput` additionally needs it **synced** (see below). ### Staging the modules directory **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_ffi.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 \ logoscore -D -m ./modules --persistence-path ./data logoscore load-module logos_execution_zone # dependency first logoscore load-module amm_module ``` Every other op reads on-chain through the wallet module's `get_account_public`, which fails on a null wallet handle (surfacing as an absent pool), so open the wallet first — `resolvePool` then works: ```bash logoscore call logos_execution_zone open ~/.lee/wallet/wallet_config.json ~/.lee/wallet/storage.json logoscore call amm_module resolvePool ``` `swapExactInput` reuses that open wallet but additionally needs it **synced** (nothing opens/syncs it for you headlessly). Note the amount/deadline conventions above — small amount bare, deadline (and any big amount) quoted: ```bash 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 a **live** sequencer read, so the stale cursor doesn't affect it — but it still needs the wallet **open**: the read goes through the wallet's sequencer connection (not its private keys), which only exists once the wallet is opened. ## 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_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.