From 3b9ca241c2301ccffd2d0b06116b7ef0c44d7d34 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Mon, 13 Jul 2026 21:01:40 +0200 Subject: [PATCH] feat(amm): metal-safe amm_client_ffi crate + root flake for on-chain swap calls --- Cargo.lock | 189 +++++++++++++++++++-- Cargo.toml | 1 + flake.lock | 98 +++++++++++ flake.nix | 72 ++++++++ programs/amm/client-ffi/Cargo.toml | 25 +++ programs/amm/client-ffi/amm_client_ffi.h | 135 +++++++++++++++ programs/amm/client-ffi/build.rs | 8 + programs/amm/client-ffi/cbindgen.toml | 7 + programs/amm/client-ffi/src/lib.rs | 203 +++++++++++++++++++++++ programs/amm/client-ffi/src/pda.rs | 48 ++++++ programs/amm/client-ffi/src/pool.rs | 47 ++++++ programs/amm/client-ffi/src/swap.rs | 52 ++++++ 12 files changed, 869 insertions(+), 16 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 programs/amm/client-ffi/Cargo.toml create mode 100644 programs/amm/client-ffi/amm_client_ffi.h create mode 100644 programs/amm/client-ffi/build.rs create mode 100644 programs/amm/client-ffi/cbindgen.toml create mode 100644 programs/amm/client-ffi/src/lib.rs create mode 100644 programs/amm/client-ffi/src/pda.rs create mode 100644 programs/amm/client-ffi/src/pool.rs create mode 100644 programs/amm/client-ffi/src/swap.rs diff --git a/Cargo.lock b/Cargo.lock index 7c3a140..52e7da3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,13 +76,27 @@ dependencies = [ "risc0-zkvm", ] +[[package]] +name = "amm_client_ffi" +version = "0.1.0" +dependencies = [ + "amm_core", + "borsh", + "cbindgen", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0-rc6)", + "risc0-binfmt", + "risc0-zkvm", + "ruint", + "twap_oracle_core", +] + [[package]] name = "amm_core" version = "0.1.0" dependencies = [ "alloy-primitives", "borsh", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "risc0-zkvm", "ruint", "serde", @@ -96,7 +110,7 @@ version = "0.1.0" dependencies = [ "amm_core", "clock_core", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "token_core", "twap_oracle_core", ] @@ -110,6 +124,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.103" @@ -480,7 +544,7 @@ name = "ata_core" version = "0.1.0" dependencies = [ "borsh", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "risc0-zkvm", "serde", ] @@ -490,7 +554,7 @@ name = "ata_program" version = "0.1.0" dependencies = [ "ata_core", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "token_core", ] @@ -802,6 +866,25 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "cbindgen" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb" +dependencies = [ + "clap", + "heck 0.4.1", + "indexmap 2.14.0", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.118", + "tempfile", + "toml", +] + [[package]] name = "cc" version = "1.2.65" @@ -858,13 +941,40 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "clock_core" version = "0.1.0" source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0#a58fbce2ff48c58b7bb5001b1a27e64b9596ee3a" dependencies = [ "borsh", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", ] [[package]] @@ -882,6 +992,12 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "const-hex" version = "1.19.1" @@ -1274,7 +1390,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "proc-macro2-diagnostics", ] @@ -1684,6 +1800,12 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -2053,7 +2175,7 @@ dependencies = [ "ata_core", "clock_core", "lee", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "stablecoin-methods", "stablecoin_core", "token-methods", @@ -2068,6 +2190,12 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -2223,7 +2351,7 @@ dependencies = [ "build_utils", "hex", "k256", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "log", "rand 0.8.6", "risc0-binfmt", @@ -2251,6 +2379,23 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "lee_core" +version = "0.1.0" +source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0-rc6#e37876a64028a335eb693198a1ed6a0e875ec5b4" +dependencies = [ + "base58", + "borsh", + "bytemuck", + "bytesize", + "chacha20", + "ml-kem", + "risc0-zkvm", + "serde", + "serde_with", + "thiserror 2.0.18", +] + [[package]] name = "libc" version = "0.2.186" @@ -2489,6 +2634,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "option-ext" version = "0.2.0" @@ -3696,7 +3847,7 @@ source = "git+https://github.com/logos-co/spel.git?tag=v0.6.0#0cb7e0980535af6194 dependencies = [ "base58", "borsh", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "proc-macro2", "serde", "serde_json", @@ -3776,7 +3927,7 @@ version = "0.1.0" dependencies = [ "alloy-primitives", "borsh", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "risc0-zkvm", "ruint", "serde", @@ -3788,7 +3939,7 @@ dependencies = [ name = "stablecoin_program" version = "0.1.0" dependencies = [ - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "stablecoin_core", "token_core", ] @@ -3820,7 +3971,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.118", @@ -4002,7 +4153,7 @@ name = "token_core" version = "0.1.0" dependencies = [ "borsh", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "serde", "spel-framework-macros", ] @@ -4011,7 +4162,7 @@ dependencies = [ name = "token_program" version = "0.1.0" dependencies = [ - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "token_core", ] @@ -4231,7 +4382,7 @@ version = "0.1.0" dependencies = [ "alloy-primitives", "borsh", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "risc0-zkvm", "ruint", "serde", @@ -4244,7 +4395,7 @@ name = "twap_oracle_program" version = "0.1.0" dependencies = [ "clock_core", - "lee_core", + "lee_core 0.1.0 (git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0)", "twap_oracle_core", ] @@ -4331,6 +4482,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index c508ac9..d38a8c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "programs/amm/core", "programs/amm", "programs/amm/methods", + "programs/amm/client-ffi", "programs/ata/core", "programs/ata", "programs/ata/methods", diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..0d6b033 --- /dev/null +++ b/flake.lock @@ -0,0 +1,98 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1783203018, + "narHash": "sha256-G6R9IT/xwFuu+CYBWDUAok6AdC4ERC4ZfPPFtEpxnZE=", + "owner": "ipetkov", + "repo": "crane", + "rev": "80db5bdc391be8a1794f6d8a2d56e3a84ebcede2", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1783776592, + "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1783921439, + "narHash": "sha256-DsSIQSRMrLOz40LrGZ03sp2RlJ9sz3wKpd8XPTOzXnw=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "e013376c32a8fcf07ddb6ec71739552bc118b7bd", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..ee63e70 --- /dev/null +++ b/flake.nix @@ -0,0 +1,72 @@ +{ + description = "LEZ programs — AMM client FFI"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + crane.url = "github:ipetkov/crane"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { self, nixpkgs, flake-utils, crane, rust-overlay }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; + # Honor the repo's pinned toolchain (rust-toolchain.toml -> 1.94.0). + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + 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_ffi`. + src = ./.; + + commonArgs = { + inherit src; + strictDeps = true; + pname = "amm_client_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_ffi` never builds + # those crates or their build scripts. + cargoExtraArgs = "-p amm_client_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 + # standalone CLI package — so no nativeBuildInputs entry is + # needed; crane builds it as part of the normal cargo graph. + }; + + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + + ammClientFfi = craneLib.buildPackage ( + commonArgs + // { + inherit cargoArtifacts; + postInstall = + '' + mkdir -p $out/include + cp programs/amm/client-ffi/amm_client_ffi.h $out/include/ + '' + + pkgs.lib.optionalString pkgs.stdenv.isDarwin '' + if [ -f $out/lib/libamm_client_ffi.dylib ]; then + install_name_tool -id @rpath/libamm_client_ffi.dylib $out/lib/libamm_client_ffi.dylib + fi + ''; + } + ); + in + { + packages.default = ammClientFfi; + packages.amm_client_ffi = ammClientFfi; + } + ); +} diff --git a/programs/amm/client-ffi/Cargo.toml b/programs/amm/client-ffi/Cargo.toml new file mode 100644 index 0000000..8211b87 --- /dev/null +++ b/programs/amm/client-ffi/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "amm_client_ffi" +version = "0.1.0" +edition = "2021" + +[lib] +name = "amm_client_ffi" +crate-type = ["staticlib", "cdylib", "rlib"] + +[lints] +workspace = true + +[dependencies] +amm_core = { path = "../core" } +twap_oracle_core = { path = "../../twap_oracle/core" } +nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc6", features = ["host"], package = "lee_core" } +borsh = { version = "1.5", features = ["derive"] } +risc0-zkvm = { version = "=3.0.5", default-features = false } +risc0-binfmt = { version = "=3.0.4", default-features = false } +# amm_core's ruint is default-features=false (for the no_std guest); enable std +# here (host-only) so its Uint::root compiles. Guest build is unaffected. +ruint = { version = "=1.17.0", default-features = false, features = ["std"] } + +[build-dependencies] +cbindgen = "0.27" diff --git a/programs/amm/client-ffi/amm_client_ffi.h b/programs/amm/client-ffi/amm_client_ffi.h new file mode 100644 index 0000000..c7ca8fa --- /dev/null +++ b/programs/amm/client-ffi/amm_client_ffi.h @@ -0,0 +1,135 @@ +#ifndef AMM_CLIENT_FFI_H +#define AMM_CLIENT_FFI_H + +#pragma once + +/* Generated by cbindgen. Do not edit. */ + +#include +#include +#include +#include + +/** + * Heap-allocated buffer of RISC0 instruction words, returned across the C + * boundary. Free with `amm_client_free_words`. + */ +typedef struct AmmWords { + uint32_t *ptr; + uintptr_t len; + bool ok; +} AmmWords; + +/** + * C-ABI representation of `nssa_core::program::ProgramId` (an Image ID), + * which is itself defined as `[u32; 8]`. Declared as a concrete array here + * (rather than an alias through `nssa_core::program::ProgramId`) so + * cbindgen — which only inspects this crate's source, not its + * dependencies' — can emit a real typedef instead of an opaque, + * self-referential `ProgramId` in the generated header. Since both are + * plain type aliases to `[u32; 8]`, they remain interchangeable to rustc. + */ +typedef uint32_t ProgramId[8]; + +/** + * C-ABI mirror of `pool::PoolView`. u128 fields are little-endian bytes. + */ +typedef struct FfiPoolView { + uint8_t def_a[32]; + uint8_t def_b[32]; + uint8_t vault_a[32]; + uint8_t vault_b[32]; + uint8_t reserve_a[16]; + uint8_t reserve_b[16]; + uint8_t liquidity_supply[16]; + uint32_t fees; + bool ok; +} FfiPoolView; + +/** + * Builds the RISC0 instruction words for a `SwapExactInput`. `amount_in` and + * `min_out` are little-endian encoded `u128` values (`[u8; 16]`). The + * returned buffer must be freed with `amm_client_free_words`. + * + * # Safety + * `amount_in` and `min_out` must be valid, non-null pointers to readable + * memory of the indicated sizes. + */ +struct AmmWords amm_client_swap_words(const uint8_t (*amount_in)[16], + const uint8_t (*min_out)[16], + uint64_t deadline); + +/** + * Frees a buffer previously returned by `amm_client_swap_words`. + * + * # Safety + * `w` must be a value previously returned by `amm_client_swap_words` that + * has not already been freed. + */ +void amm_client_free_words(struct AmmWords w); + +/** + * Computes the `ProgramId` (Image ID) of a compiled guest ELF. Returns + * `false` on an invalid ELF, leaving `out` unwritten. + * + * # Safety + * `elf` must be valid for reads of `elf_len` bytes, and `out` must be a + * valid, non-null pointer to writable memory for a `ProgramId`. + */ +bool amm_client_program_id_from_elf(const uint8_t *elf, uintptr_t elf_len, ProgramId *out); + +/** + * Fills `out` with the AMM config PDA. + * + * # Safety + * `amm` must be a valid, non-null pointer to a readable `ProgramId`, and + * `out` must be a valid, non-null pointer to writable memory. + */ +void amm_client_config_pda(const ProgramId *amm, uint8_t (*out)[32]); + +/** + * Fills `out` with the pool PDA for the two definition ids. + * + * # Safety + * All pointer arguments must be valid, non-null, and point to readable (or, + * for `out`, writable) memory of the indicated sizes. + */ +void amm_client_pool_pda(const ProgramId *amm, + const uint8_t (*def_a)[32], + const uint8_t (*def_b)[32], + uint8_t (*out)[32]); + +/** + * Fills `out` with the vault PDA for a pool + token definition. + * + * # Safety + * All pointer arguments must be valid, non-null, and point to readable (or, + * for `out`, writable) memory of the indicated sizes. + */ +void amm_client_vault_pda(const ProgramId *amm, + const uint8_t (*pool)[32], + const uint8_t (*def)[32], + uint8_t (*out)[32]); + +/** + * Fills `out` with the TWAP oracle current-tick PDA for a pool. + * + * # Safety + * All pointer arguments must be valid, non-null, and point to readable (or, + * for `out`, writable) memory of the indicated sizes. + */ +void amm_client_current_tick_pda(const ProgramId *twap, + const uint8_t (*pool)[32], + uint8_t (*out)[32]); + +/** + * Decodes a `PoolDefinition` account's raw bytes into `out`. Returns `false` + * on decode failure, leaving `out` unwritten. + * + * # Safety + * `bytes` must be valid for reads of `len` bytes, and `out` must be a + * valid, non-null pointer to writable memory for a `FfiPoolView`. + */ +bool amm_client_decode_pool(const uint8_t *bytes, uintptr_t len, struct FfiPoolView *out); + +#endif /* AMM_CLIENT_FFI_H */ diff --git a/programs/amm/client-ffi/build.rs b/programs/amm/client-ffi/build.rs new file mode 100644 index 0000000..32946d2 --- /dev/null +++ b/programs/amm/client-ffi/build.rs @@ -0,0 +1,8 @@ +fn main() { + let crate_dir = + 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}/amm_client_ffi.h")); + println!("cargo:rerun-if-changed=src/lib.rs"); +} diff --git a/programs/amm/client-ffi/cbindgen.toml b/programs/amm/client-ffi/cbindgen.toml new file mode 100644 index 0000000..5d67708 --- /dev/null +++ b/programs/amm/client-ffi/cbindgen.toml @@ -0,0 +1,7 @@ +language = "C" +include_guard = "AMM_CLIENT_FFI_H" +pragma_once = true +autogen_warning = "/* Generated by cbindgen. Do not edit. */" + +[export] +prefix = "" diff --git a/programs/amm/client-ffi/src/lib.rs b/programs/amm/client-ffi/src/lib.rs new file mode 100644 index 0000000..cec0f4e --- /dev/null +++ b/programs/amm/client-ffi/src/lib.rs @@ -0,0 +1,203 @@ +//! C-ABI client helpers for calling the AMM program from host apps. +//! All AMM instruction/PDA logic is delegated to `amm_core`; encoding is +//! delegated to `lee` — this crate only bridges to a C ABI. +#![allow( + unsafe_code, + reason = "this crate exists solely to expose a C ABI; every unsafe fn \ + is a documented pointer dereference at the FFI boundary" +)] + +mod pda; +mod pool; +mod swap; + +use nssa_core::account::AccountId; + +/// C-ABI representation of `nssa_core::program::ProgramId` (an Image ID), +/// which is itself defined as `[u32; 8]`. Declared as a concrete array here +/// (rather than an alias through `nssa_core::program::ProgramId`) so +/// cbindgen — which only inspects this crate's source, not its +/// dependencies' — can emit a real typedef instead of an opaque, +/// self-referential `ProgramId` in the generated header. Since both are +/// plain type aliases to `[u32; 8]`, they remain interchangeable to rustc. +pub type ProgramId = [u32; 8]; + +/// Heap-allocated buffer of RISC0 instruction words, returned across the C +/// boundary. Free with `amm_client_free_words`. +#[repr(C)] +pub struct AmmWords { + pub ptr: *mut u32, + pub len: usize, + pub ok: bool, +} + +/// C-ABI mirror of `pool::PoolView`. u128 fields are little-endian bytes. +#[repr(C)] +pub struct FfiPoolView { + pub def_a: [u8; 32], + pub def_b: [u8; 32], + pub vault_a: [u8; 32], + pub vault_b: [u8; 32], + pub reserve_a: [u8; 16], + pub reserve_b: [u8; 16], + pub liquidity_supply: [u8; 16], + pub fees: u32, + pub ok: bool, +} + +/// # Safety +/// `p` must be a valid, non-null pointer to a readable `[u8; 32]`. +unsafe fn acc(p: *const [u8; 32]) -> AccountId { + AccountId::new(*p) +} + +/// Builds the RISC0 instruction words for a `SwapExactInput`. `amount_in` and +/// `min_out` are little-endian encoded `u128` values (`[u8; 16]`). The +/// returned buffer must be freed with `amm_client_free_words`. +/// +/// # Safety +/// `amount_in` and `min_out` must be valid, non-null pointers to readable +/// memory of the indicated sizes. +#[no_mangle] +pub unsafe extern "C" fn amm_client_swap_words( + amount_in: *const [u8; 16], + min_out: *const [u8; 16], + deadline: u64, +) -> AmmWords { + let (a, m) = (u128::from_le_bytes(*amount_in), u128::from_le_bytes(*min_out)); + match swap::swap_exact_input_words(a, m, deadline) { + Ok(w) => { + let boxed = w.into_boxed_slice(); + let len = boxed.len(); + let ptr = Box::into_raw(boxed).cast::(); + AmmWords { ptr, len, ok: true } + } + Err(_) => AmmWords { + ptr: core::ptr::null_mut(), + len: 0, + ok: false, + }, + } +} + +/// Frees a buffer previously returned by `amm_client_swap_words`. +/// +/// # Safety +/// `w` must be a value previously returned by `amm_client_swap_words` that +/// has not already been freed. +#[no_mangle] +pub unsafe extern "C" fn amm_client_free_words(w: AmmWords) { + if !w.ptr.is_null() { + drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut( + w.ptr, w.len, + ))); + } +} + +/// Computes the `ProgramId` (Image ID) of a compiled guest ELF. Returns +/// `false` on an invalid ELF, leaving `out` unwritten. +/// +/// # Safety +/// `elf` must be valid for reads of `elf_len` bytes, and `out` must be a +/// valid, non-null pointer to writable memory for a `ProgramId`. +#[no_mangle] +pub unsafe extern "C" fn amm_client_program_id_from_elf( + elf: *const u8, + elf_len: usize, + out: *mut ProgramId, +) -> bool { + let bytes = core::slice::from_raw_parts(elf, elf_len); + match pda::program_id_from_elf(bytes) { + Ok(id) => { + *out = id; + true + } + Err(_) => false, + } +} + +/// Fills `out` with the AMM config PDA. +/// +/// # Safety +/// `amm` must be a valid, non-null pointer to a readable `ProgramId`, and +/// `out` must be a valid, non-null pointer to writable memory. +#[no_mangle] +pub unsafe extern "C" fn amm_client_config_pda(amm: *const ProgramId, out: *mut [u8; 32]) { + *out = pda::config_pda(*amm).into_value(); +} + +/// Fills `out` with the pool PDA for the two definition ids. +/// +/// # Safety +/// All pointer arguments must be valid, non-null, and point to readable (or, +/// for `out`, writable) memory of the indicated sizes. +#[no_mangle] +pub unsafe extern "C" fn amm_client_pool_pda( + amm: *const ProgramId, + def_a: *const [u8; 32], + def_b: *const [u8; 32], + out: *mut [u8; 32], +) { + *out = pda::pool_pda(*amm, acc(def_a), acc(def_b)).into_value(); +} + +/// Fills `out` with the vault PDA for a pool + token definition. +/// +/// # Safety +/// All pointer arguments must be valid, non-null, and point to readable (or, +/// for `out`, writable) memory of the indicated sizes. +#[no_mangle] +pub unsafe extern "C" fn amm_client_vault_pda( + amm: *const ProgramId, + pool: *const [u8; 32], + def: *const [u8; 32], + out: *mut [u8; 32], +) { + *out = pda::vault_pda(*amm, acc(pool), acc(def)).into_value(); +} + +/// Fills `out` with the TWAP oracle current-tick PDA for a pool. +/// +/// # Safety +/// All pointer arguments must be valid, non-null, and point to readable (or, +/// for `out`, writable) memory of the indicated sizes. +#[no_mangle] +pub unsafe extern "C" fn amm_client_current_tick_pda( + twap: *const ProgramId, + pool: *const [u8; 32], + out: *mut [u8; 32], +) { + *out = pda::current_tick_pda(*twap, acc(pool)).into_value(); +} + +/// Decodes a `PoolDefinition` account's raw bytes into `out`. Returns `false` +/// on decode failure, leaving `out` unwritten. +/// +/// # Safety +/// `bytes` must be valid for reads of `len` bytes, and `out` must be a +/// valid, non-null pointer to writable memory for a `FfiPoolView`. +#[no_mangle] +pub unsafe extern "C" fn amm_client_decode_pool( + bytes: *const u8, + len: usize, + out: *mut FfiPoolView, +) -> bool { + let b = core::slice::from_raw_parts(bytes, len); + match pool::decode_pool(b) { + Ok(v) => { + *out = FfiPoolView { + def_a: v.def_a, + def_b: v.def_b, + vault_a: v.vault_a, + vault_b: v.vault_b, + reserve_a: v.reserve_a.to_le_bytes(), + reserve_b: v.reserve_b.to_le_bytes(), + liquidity_supply: v.liquidity_supply.to_le_bytes(), + fees: v.fees, + ok: true, + }; + true + } + Err(_) => false, + } +} diff --git a/programs/amm/client-ffi/src/pda.rs b/programs/amm/client-ffi/src/pda.rs new file mode 100644 index 0000000..2715253 --- /dev/null +++ b/programs/amm/client-ffi/src/pda.rs @@ -0,0 +1,48 @@ +//! PDA derivation and program-id helpers, delegating to `amm_core` / +//! `twap_oracle_core` so the client never re-implements seed hashing. + +use amm_core::{compute_config_pda, compute_pool_pda, compute_vault_pda}; +use nssa_core::{account::AccountId, program::ProgramId}; +use risc0_binfmt::ProgramBinary; +use twap_oracle_core::compute_current_tick_account_pda; + +/// Computes the `ProgramId` (Image ID) of a compiled guest ELF, the same way +/// the sequencer/wallet does when deploying a program. +pub fn program_id_from_elf(elf: &[u8]) -> Result { + let binary = ProgramBinary::decode(elf).map_err(|e| format!("{e:?}"))?; + let id = binary.compute_image_id().map_err(|e| format!("{e:?}"))?; + Ok(id.into()) +} + +pub fn config_pda(amm: ProgramId) -> AccountId { + compute_config_pda(amm) +} + +pub fn pool_pda(amm: ProgramId, def_a: AccountId, def_b: AccountId) -> AccountId { + compute_pool_pda(amm, def_a, def_b) +} + +pub fn vault_pda(amm: ProgramId, pool: AccountId, def: AccountId) -> AccountId { + compute_vault_pda(amm, pool, def) +} + +pub fn current_tick_pda(twap: ProgramId, pool: AccountId) -> AccountId { + compute_current_tick_account_pda(twap, pool) +} + +#[cfg(test)] +mod tests { + use amm_core::compute_pool_pda; + use nssa_core::account::AccountId; + use nssa_core::program::ProgramId; + + use super::*; + + #[test] + fn pool_pda_matches_core() { + let amm: ProgramId = [1u32; 8]; + let a = AccountId::new([2u8; 32]); + let b = AccountId::new([3u8; 32]); + assert_eq!(pool_pda(amm, a, b), compute_pool_pda(amm, a, b)); + } +} diff --git a/programs/amm/client-ffi/src/pool.rs b/programs/amm/client-ffi/src/pool.rs new file mode 100644 index 0000000..a028032 --- /dev/null +++ b/programs/amm/client-ffi/src/pool.rs @@ -0,0 +1,47 @@ +//! Decoding for the AMM's on-chain `PoolDefinition` account, so client apps +//! can read reserves / fees / vault ids without depending on `amm_core` +//! directly. + +use amm_core::PoolDefinition; + +pub struct PoolView { + pub def_a: [u8; 32], + pub def_b: [u8; 32], + pub vault_a: [u8; 32], + pub vault_b: [u8; 32], + pub reserve_a: u128, + pub reserve_b: u128, + pub liquidity_supply: u128, + /// Fee tier in basis points. Source is `u128` but supported tiers are all + /// <= 100, so downcasting to `u32` is safe. + pub fees: u32, +} + +pub fn decode_pool(bytes: &[u8]) -> Result { + let p: PoolDefinition = borsh::from_slice(bytes).map_err(|e| format!("{e:?}"))?; + Ok(PoolView { + def_a: p.definition_token_a_id.into_value(), + def_b: p.definition_token_b_id.into_value(), + vault_a: p.vault_a_id.into_value(), + vault_b: p.vault_b_id.into_value(), + reserve_a: p.reserve_a, + reserve_b: p.reserve_b, + liquidity_supply: p.liquidity_pool_supply, + fees: u32::try_from(p.fees).unwrap_or(u32::MAX), + }) +} + +#[cfg(test)] +mod tests { + use amm_core::PoolDefinition; + + use super::*; + + #[test] + fn decode_roundtrip() { + let p = PoolDefinition::default(); + let bytes = borsh::to_vec(&p).unwrap(); + let v = decode_pool(&bytes).unwrap(); + assert_eq!(v.reserve_a, 0); + } +} diff --git a/programs/amm/client-ffi/src/swap.rs b/programs/amm/client-ffi/src/swap.rs new file mode 100644 index 0000000..81fab76 --- /dev/null +++ b/programs/amm/client-ffi/src/swap.rs @@ -0,0 +1,52 @@ +use amm_core::Instruction; + +/// Build the RISC0 instruction words for a SwapExactInput, via the exact +/// serializer the public-transaction path and the guest decoder share. +pub fn swap_exact_input_words( + amount_in: u128, + min_out: u128, + deadline: u64, +) -> Result, String> { + let instruction = Instruction::SwapExactInput { + swap_amount_in: amount_in, + min_amount_out: min_out, + deadline, + }; + risc0_zkvm::serde::to_vec(&instruction).map_err(|e| format!("{e:?}")) +} + +#[cfg(test)] +mod tests { + use amm_core::Instruction; + + use super::*; + + #[test] + fn words_roundtrip_to_same_instruction() { + let words = swap_exact_input_words(1000, 1, u64::MAX).unwrap(); + // Deserialize back through the exact serde the guest decoder uses. + // `Instruction` derives neither `Debug` nor `PartialEq`, so assert + // field-by-field instead of matching with a `{:?}` fallback arm. + let decoded: Instruction = risc0_zkvm::serde::from_slice(&words).unwrap(); + match decoded { + Instruction::SwapExactInput { + swap_amount_in, + min_amount_out, + deadline, + } => { + assert_eq!(swap_amount_in, 1000); + assert_eq!(min_amount_out, 1); + assert_eq!(deadline, u64::MAX); + } + Instruction::Initialize { .. } + | Instruction::UpdateConfig { .. } + | Instruction::CreatePriceObservations { .. } + | Instruction::CreateOraclePriceAccount { .. } + | Instruction::NewDefinition { .. } + | Instruction::AddLiquidity { .. } + | Instruction::RemoveLiquidity { .. } + | Instruction::SwapExactOutput { .. } + | Instruction::SyncReserves => panic!("wrong variant: expected SwapExactInput"), + } + } +}