mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
Token discovery is an app concern, not module business — same rationale as poolList reading AMM_POOLS_CONFIG. Drop tokenList() from amm_module and have the app read the config itself.
263 lines
12 KiB
Markdown
263 lines
12 KiB
Markdown
# 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 <defA_hex> <defB_hex>
|
|
```
|
|
|
|
`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 \
|
|
<defA_hex> <defB_hex> <inputHolding_hex> <outputHolding_hex> \
|
|
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 <height>` 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<uint8_t>`). 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.
|