Consolidate wallet account decoding and portfolio handling around the shared Rust IDL decoder. Simplify AMM wallet integration, remove obsolete caches and network plumbing, and cover account selection and live flows.
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 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:
Reads
resolvePoolAccount(defAHex, defBHex)— derives the pool PDA and reads/decodes the pool account (reserves in canonicala/border, 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).configAccount()— decodes the singleton AMM config (authority + the token/oracle program ids it was initialized with).feeTiers()— the AMM's supported fee tiers as raw basis points[1, 5, 30, 100].tokenHoldings(walletOpen)— the connected wallet's fungible token holdings.resolveTokens(request, walletOpen)— resolves an app-provided set of token ids into selector rows (definition + wallet holding per id). The app owns the id set, so there is no network envelope or process-cached wallet state here.
Quotes (read-only pricing; no submit)
swapExactInQuote/swapExactOutQuote— price a swap.createPoolQuote/addLiquidityQuote/removeLiquidityQuote— price a liquidity op.
Submits (on-chain transactions; return { status, error, transactionId },
except the two swaps which return a bare tx hash)
swapExactInput/swapExactOutput(defA = token in, defB = token out).createPool/addLiquidity/removeLiquidity— the create/add paths take a freshlpHoldingIdthe app supplies (the module never creates wallet accounts).syncReserves— permissionless keeper op refreshing stored reserves + TWAP tick.createPriceObservations/createOraclePriceAccount— seed a pool's TWAP feed.transferOwnership— admin-onlyUpdateConfighanding over the authority.
See Amount / id conventions below, and the
full logoscore runbook for a worked call per method.
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.
1e18base 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):
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 deployedamm.bin. Required; its ELF determines the program id and every derived PDA. Without it,resolvePoolAccountreturns{ 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:
-
Nix with flakes enabled (same as the rest of the repo).
-
The runtime CLIs (from their own flakes, per the developer guide):
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)lmintrospects a built plugin without running it — handy to confirm the API (lm result/lib/amm_module_plugin.dylibshows methods, signatures, deps). -
This module, built (produces
amm_module_plugin.dylib+libamm_ffi.dylib):nix build .#amm-module # from the repo root; output under result/lib/ -
The wallet module it depends on, built —
logos_execution_zoneis a separate repo, not part of this tree. Build the same rev this module pins as itslogos_execution_zoneflake input (mismatched revs = ABI/ImageID drift), producinglogos_execution_zone_plugin.dylib+libwallet_ffi.dylib: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) -
The deployed
amm.binforAMM_PROGRAM_BIN— the exact binary running on your target sequencer (its ELF fixes the program id and every PDA). Seeapps/amm/README.mdand the testnet runbook. -
A wallet at
~/.lee/wallet(wallet_config.jsonwithsequencer_addrpointing at your sequencer, plusstorage.jsonwith your accounts), and a running sequencer with the AMM initialized and a pool holding liquidity. Every op (includingresolvePoolAccount) reads on-chain through the wallet module'sget_account_public, which needs the wallet open (the handle is null untilopen/create_new);swapExactInputadditionally 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:
{
"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:
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 — resolvePoolAccount then works:
logoscore call logos_execution_zone open ~/.lee/wallet/wallet_config.json ~/.lee/wallet/storage.json
logoscore call amm_module resolvePoolAccount <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:
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. resolvePoolAccount 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.
Full API runbook
The method list above is a curated subset. For a complete, worked logoscore
walkthrough of every amm_module API — reads, swaps, add/remove liquidity,
the keeper syncReserves, oracle setup, and admin — see
docs/module/amm.md.