refactor(apps/amm): move the amm_client crate into modules/amm/ffi as amm_ffi

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)
This commit is contained in:
r4bbit
2026-08-03 23:02:15 +02:00
parent afeba568d8
commit dac442435a
38 changed files with 83 additions and 94 deletions
Generated
+15 -15
View File
@@ -77,7 +77,21 @@ dependencies = [
]
[[package]]
name = "amm_client"
name = "amm_core"
version = "0.1.0"
dependencies = [
"alloy-primitives",
"borsh",
"lee_core",
"risc0-zkvm",
"ruint",
"serde",
"spel-framework-macros",
"token_core",
]
[[package]]
name = "amm_ffi"
version = "0.1.0"
dependencies = [
"alloy-primitives",
@@ -97,20 +111,6 @@ dependencies = [
"twap_oracle_core",
]
[[package]]
name = "amm_core"
version = "0.1.0"
dependencies = [
"alloy-primitives",
"borsh",
"lee_core",
"risc0-zkvm",
"ruint",
"serde",
"spel-framework-macros",
"token_core",
]
[[package]]
name = "amm_program"
version = "0.1.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace]
members = [
"apps/amm/client",
"modules/amm/ffi",
"programs/token/core",
"programs/token",
"programs/token/methods",
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 .#<name>` — there is no bare `nix run .`
default.)
+2 -2
View File
@@ -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]')
+8 -19
View File
@@ -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.
+1 -1
View File
@@ -74,7 +74,7 @@ private:
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.
+21 -21
View File
@@ -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} "$@"
''}";
};
+3 -3
View File
@@ -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
)
+8 -8
View File
@@ -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/
@@ -168,7 +168,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/
@@ -268,6 +268,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.
@@ -1,5 +1,5 @@
[package]
name = "amm_client"
name = "amm_ffi"
version = "0.1.0"
edition = "2021"
@@ -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");
}
@@ -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. */"
@@ -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 */
@@ -1,4 +1,4 @@
use amm_client::{config_id, ConfigIdRequest};
use amm_ffi::{config_id, ConfigIdRequest};
#[test]
fn direct_rust_api_does_not_require_ffi() {
+2 -2
View File
@@ -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, ... }:
+1 -1
View File
@@ -14,7 +14,7 @@
"runtime": []
},
"external_libraries": [
{ "name": "amm_client" }
{ "name": "amm_ffi" }
],
"cmake": {
"find_packages": [],
+8 -8
View File
@@ -20,7 +20,7 @@
#include "logos_sdk.h"
extern "C" {
#include "amm_client.h"
#include "amm_ffi.h"
}
namespace {
@@ -193,20 +193,20 @@ std::vector<uint8_t> 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);
@@ -214,16 +214,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};
@@ -294,7 +294,7 @@ std::vector<uint8_t> AmmModuleImpl::loadAmmElf() {
std::string AmmModuleImpl::ammProgramId() {
const std::vector<uint8_t> 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())}});
+4 -4
View File
@@ -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).
@@ -108,7 +108,7 @@ private:
// success, so a startup miss (bin not readable yet) retries.
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();
@@ -120,13 +120,13 @@ private:
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);