diff --git a/Cargo.lock b/Cargo.lock index 5da900e2..0147201a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,10 +83,12 @@ dependencies = [ "alloy-primitives", "amm_core", "borsh", + "cbindgen", "clock_core", "hex", "lee_core", "pretty_assertions", + "risc0-binfmt", "risc0-zkvm", "serde", "serde_json", @@ -95,20 +97,6 @@ dependencies = [ "twap_oracle_core", ] -[[package]] -name = "amm_client_ffi" -version = "0.1.0" -dependencies = [ - "amm_core", - "borsh", - "cbindgen", - "lee_core", - "risc0-binfmt", - "risc0-zkvm", - "ruint", - "twap_oracle_core", -] - [[package]] name = "amm_core" version = "0.1.0" @@ -887,9 +875,9 @@ dependencies = [ [[package]] name = "cbindgen" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" dependencies = [ "clap", "heck 0.4.1", diff --git a/Cargo.toml b/Cargo.toml index 5631a811..35a1dee8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ "programs/amm/core", "programs/amm", "programs/amm/methods", - "programs/amm/client-ffi", "programs/ata/core", "programs/ata", "programs/ata/methods", diff --git a/apps/amm/CMakeLists.txt b/apps/amm/CMakeLists.txt index 6e8e4370..7f0121c1 100644 --- a/apps/amm/CMakeLists.txt +++ b/apps/amm/CMakeLists.txt @@ -41,6 +41,8 @@ logos_module( src/AmmClient.cpp src/NewPositionRuntime.h src/NewPositionRuntime.cpp + src/SwapRuntime.h + src/SwapRuntime.cpp FIND_PACKAGES Qt6Gui LINK_LIBRARIES @@ -49,7 +51,6 @@ logos_module( LINK_TARGETS logos_wallet_access EXTERNAL_LIBS - amm_client_ffi amm_client ) diff --git a/apps/amm/client/Cargo.toml b/apps/amm/client/Cargo.toml index eb5014cd..d3e2be76 100644 --- a/apps/amm/client/Cargo.toml +++ b/apps/amm/client/Cargo.toml @@ -13,6 +13,7 @@ borsh = { workspace = true } clock_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0" } hex = "0.4" nssa_core = { workspace = true } +risc0-binfmt = { version = "=3.0.4", default-features = false } risc0-zkvm = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -20,5 +21,8 @@ sha2 = "0.10" token_core = { workspace = true } twap_oracle_core = { workspace = true } +[build-dependencies] +cbindgen = "0.28" + [dev-dependencies] pretty_assertions = "1" diff --git a/programs/amm/client-ffi/build.rs b/apps/amm/client/build.rs similarity index 71% rename from programs/amm/client-ffi/build.rs rename to apps/amm/client/build.rs index 5c7aa76f..0ca66455 100644 --- a/programs/amm/client-ffi/build.rs +++ b/apps/amm/client/build.rs @@ -3,8 +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}/amm_client_ffi.h")); - println!("cargo:rerun-if-changed=src/lib.rs"); + .write_to_file(format!("{crate_dir}/include/amm_client.h")); println!("cargo:rerun-if-changed=src"); println!("cargo:rerun-if-changed=cbindgen.toml"); } diff --git a/programs/amm/client-ffi/cbindgen.toml b/apps/amm/client/cbindgen.toml similarity index 70% rename from programs/amm/client-ffi/cbindgen.toml rename to apps/amm/client/cbindgen.toml index 5d67708a..210f8e52 100644 --- a/programs/amm/client-ffi/cbindgen.toml +++ b/apps/amm/client/cbindgen.toml @@ -1,6 +1,7 @@ language = "C" -include_guard = "AMM_CLIENT_FFI_H" +include_guard = "AMM_CLIENT_H" pragma_once = true +cpp_compat = true autogen_warning = "/* Generated by cbindgen. Do not edit. */" [export] diff --git a/apps/amm/client/include/amm_client.h b/apps/amm/client/include/amm_client.h index 83c56a6d..5b2eb025 100644 --- a/apps/amm/client/include/amm_client.h +++ b/apps/amm/client/include/amm_client.h @@ -1,20 +1,49 @@ #ifndef AMM_CLIENT_H #define AMM_CLIENT_H +#pragma once + +/* Generated by cbindgen. Do not edit. */ + +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { -#endif +#endif // __cplusplus char *amm_config_id(const char *request_json); + char *amm_token_ids(const char *request_json); + char *amm_pair_ids(const char *request_json); + char *amm_context(const char *request_json); + char *amm_quote(const char *request_json); + char *amm_plan(const char *request_json); + +char *amm_swap_pair(const char *request_json); + +char *amm_resolve_pool(const char *request_json); + +char *amm_swap_plan(const char *request_json); + +char *amm_program_id(const char *request_json); + +/** + * Releases a string returned by an `amm_*` operation. + * + * # Safety + * `value` must be null or a pointer returned by this library that has not been freed. + */ void amm_free(char *value); #ifdef __cplusplus -} -#endif +} // extern "C" +#endif // __cplusplus -#endif +#endif /* AMM_CLIENT_H */ diff --git a/apps/amm/client/src/api/mod.rs b/apps/amm/client/src/api/mod.rs index fdae2431..c7745110 100644 --- a/apps/amm/client/src/api/mod.rs +++ b/apps/amm/client/src/api/mod.rs @@ -13,6 +13,7 @@ mod position; mod quote; mod quote_error; mod request; +mod swap; #[cfg(test)] mod tests; @@ -21,7 +22,8 @@ use std::{error::Error, fmt}; pub use request::{ ConfigIdRequest, ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest, PositionRequest, - QuoteRequest, TokenIdsRequest, + ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapPairRequest, SwapPlanRequest, + TokenIdsRequest, }; use serde_json::Value; @@ -95,3 +97,23 @@ pub fn quote(request: QuoteRequest) -> AmmResult { pub fn plan(request: PlanRequest) -> AmmResult { plan::plan(request).map_err(Into::into) } + +/// Derives the canonical account ids for a swap pair (tokens in either order). +pub fn swap_pair(request: SwapPairRequest) -> AmmResult { + swap::swap_pair(request).map_err(Into::into) +} + +/// Decodes a pool account: existence, reserves (canonical order), fee tier. +pub fn resolve_pool(request: ResolvePoolRequest) -> AmmResult { + swap::resolve_pool(request).map_err(Into::into) +} + +/// Builds the `SwapExactInput` wallet submission for a token pair. +pub fn swap_plan(request: SwapPlanRequest) -> AmmResult { + swap::swap_plan(request).map_err(Into::into) +} + +/// Derives the AMM `ProgramId` (Image ID) from a deployed program binary. +pub fn program_id(request: ProgramIdRequest) -> AmmResult { + swap::program_id(request).map_err(Into::into) +} diff --git a/apps/amm/client/src/api/request.rs b/apps/amm/client/src/api/request.rs index 082b143d..883e8174 100644 --- a/apps/amm/client/src/api/request.rs +++ b/apps/amm/client/src/api/request.rs @@ -52,6 +52,41 @@ pub struct PairIdsRequest { pub token_b_id: String, } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SwapPairRequest { + pub amm_program_id: String, + pub token_in_id: String, + pub token_out_id: String, + pub config: AccountRead, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ResolvePoolRequest { + pub pool: AccountRead, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SwapPlanRequest { + pub amm_program_id: String, + pub token_in_id: String, + pub token_out_id: String, + pub config: AccountRead, + pub user_input_holding_id: String, + pub user_output_holding_id: String, + pub amount_in: String, + pub min_out: String, + pub deadline_ms: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ProgramIdRequest { + pub elf: String, +} + #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PositionRequest { diff --git a/apps/amm/client/src/api/swap.rs b/apps/amm/client/src/api/swap.rs new file mode 100644 index 00000000..5dcd45d9 --- /dev/null +++ b/apps/amm/client/src/api/swap.rs @@ -0,0 +1,157 @@ +//! Swap operations — pool discovery/decode, swap-transaction planning, and +//! program-id derivation. Same transport-independent pattern as the +//! new-position ops: pure functions returning JSON `Value`, PDAs reused from +//! `pair::derive_pair` so the swap path never re-derives seeds. + +use amm_core::PoolDefinition; +use nssa_core::account::AccountId; +use risc0_binfmt::ProgramBinary; +use serde_json::{json, Value}; + +use super::{ + pair::{derive_pair, is_canonical_pair}, + ProgramIdRequest, ResolvePoolRequest, SwapPairRequest, SwapPlanRequest, +}; +use crate::account::{ + account_id_from_hex, account_id_hex, decode_account, parse_program_id, program_id_bytes, +}; + +/// Orders `(token_in, token_out)` into the pool's canonical `(token_a, token_b)` +/// so derived vault PDAs line up with the pool's stored `vault_a`/`vault_b`. +fn canonical_pair(token_in: AccountId, token_out: AccountId) -> (AccountId, AccountId) { + if is_canonical_pair(token_in, token_out) { + (token_in, token_out) + } else { + (token_out, token_in) + } +} + +fn parse_u128(value: &str, label: &str) -> Result { + value + .parse::() + .map_err(|error| format!("invalid {label}: {error}")) +} + +fn parse_u64(value: &str, label: &str) -> Result { + value + .parse::() + .map_err(|error| format!("invalid {label}: {error}")) +} + +/// Derives the canonical account ids for a swap pair — reuses `derive_pair` +/// after ordering `(token_in, token_out)` into `(token_a, token_b)`, so a +/// caller can read the pool account before decoding it. Like `pair_ids`, but +/// accepts the tokens in either order. +pub(super) fn swap_pair(request: SwapPairRequest) -> Result { + let amm_program = parse_program_id(&request.amm_program_id)?; + let token_in = account_id_from_hex(&request.token_in_id, "token in id")?; + let token_out = account_id_from_hex(&request.token_out_id, "token out id")?; + if token_in == token_out { + return Ok(json!({ "status": "error", "code": "same_token_pair" })); + } + let (token_a, token_b) = canonical_pair(token_in, token_out); + let Ok(pair) = derive_pair(amm_program, token_a, token_b, &request.config) else { + return Ok(json!({ "status": "error", "code": "config_unavailable" })); + }; + Ok(json!({ + "status": "ok", + "configId": account_id_hex(pair.config), + "poolId": account_id_hex(pair.pool), + "vaultAId": account_id_hex(pair.vault_a), + "vaultBId": account_id_hex(pair.vault_b), + "currentTickId": account_id_hex(pair.current_tick), + "clockId": account_id_hex(pair.clock), + })) +} + +/// Decodes a pool account: whether it holds liquidity, its reserves (canonical +/// `a`/`b` order — the caller orients to its own direction), and fee tier. +/// Absent/empty/uninitialized pool → `{ exists: false }`. +pub(super) fn resolve_pool(request: ResolvePoolRequest) -> Result { + if request.pool.status != "ok" { + return Ok(json!({ "exists": false })); + } + let Ok((_, pool_account)) = decode_account(&request.pool) else { + return Ok(json!({ "exists": false })); + }; + let Ok(pool) = PoolDefinition::try_from(&pool_account.data) else { + return Ok(json!({ "exists": false })); + }; + if pool.liquidity_pool_supply == 0 { + return Ok(json!({ "exists": false })); + } + let fee_bps = + u32::try_from(pool.fees).map_err(|_| String::from("pool fee tier exceeds u32"))?; + Ok(json!({ + "exists": true, + "reserveA": pool.reserve_a.to_string(), + "reserveB": pool.reserve_b.to_string(), + "feeBps": fee_bps, + })) +} + +/// Builds the `SwapExactInput` submission for a pair: the fixed 8-account IDL +/// order (vaults canonical, only the user's input holding signs) and the +/// instruction words (`risc0_zkvm::serde` — the same encoding the guest +/// decodes). Mirrors `plan.rs`'s `ready` output shape. +pub(super) fn swap_plan(request: SwapPlanRequest) -> Result { + let amm_program = parse_program_id(&request.amm_program_id)?; + let token_in = account_id_from_hex(&request.token_in_id, "token in id")?; + let token_out = account_id_from_hex(&request.token_out_id, "token out id")?; + if token_in == token_out { + return Err(String::from("swap requires two distinct tokens")); + } + let (token_a, token_b) = canonical_pair(token_in, token_out); + let pair = derive_pair(amm_program, token_a, token_b, &request.config)?; + + let user_input_holding = + account_id_from_hex(&request.user_input_holding_id, "user input holding id")?; + let user_output_holding = + account_id_from_hex(&request.user_output_holding_id, "user output holding id")?; + + let swap_amount_in = parse_u128(&request.amount_in, "amountIn")?; + let min_amount_out = parse_u128(&request.min_out, "minOut")?; + let deadline = parse_u64(&request.deadline_ms, "deadlineMs")?; + + let instruction = risc0_zkvm::serde::to_vec(&amm_core::Instruction::SwapExactInput { + swap_amount_in, + min_amount_out, + deadline, + }) + .map_err(|error| format!("instruction serialization failed: {error}"))?; + + // Fixed IDL account order for SwapExactInput; only user_input_holding signs. + let account_ids = [ + pair.config, + pair.pool, + pair.vault_a, + pair.vault_b, + user_input_holding, + user_output_holding, + pair.current_tick, + pair.clock, + ]; + let signing_requirements = [false, false, false, false, true, false, false, false]; + + Ok(json!({ + "status": "ready", + "programId": request.amm_program_id, + "accountIds": account_ids.into_iter().map(account_id_hex).collect::>(), + "signingRequirements": signing_requirements, + "instruction": instruction, + "deadlineMs": deadline.to_string(), + })) +} + +/// Computes the AMM `ProgramId` (RISC Zero Image ID) of a deployed program +/// binary. `elf` is the hex-encoded `.bin` (a RISC Zero `ProgramBinary`, not a +/// raw ELF) — decoded, image-id computed, returned as 64-char lowercase hex. +pub(super) fn program_id(request: ProgramIdRequest) -> Result { + let elf = hex::decode(&request.elf).map_err(|error| format!("invalid elf hex: {error}"))?; + let binary = ProgramBinary::decode(&elf).map_err(|error| format!("{error:?}"))?; + let image_id: nssa_core::program::ProgramId = binary + .compute_image_id() + .map_err(|error| format!("{error:?}"))? + .into(); + Ok(json!({ "programId": hex::encode(program_id_bytes(image_id)) })) +} diff --git a/apps/amm/client/src/ffi.rs b/apps/amm/client/src/ffi.rs index 552e9508..bd3ea260 100644 --- a/apps/amm/client/src/ffi.rs +++ b/apps/amm/client/src/ffi.rs @@ -7,7 +7,8 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::api::{ self, AmmApiError, AmmResult, ConfigIdRequest, ContextRequest, PairIdsRequest, PlanRequest, - QuoteRequest, TokenIdsRequest, + ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapPairRequest, SwapPlanRequest, + TokenIdsRequest, }; #[derive(Serialize)] @@ -106,6 +107,26 @@ pub extern "C" fn amm_plan(request_json: *const c_char) -> *mut c_char { call::(request_json, api::plan) } +#[unsafe(no_mangle)] +pub extern "C" fn amm_swap_pair(request_json: *const c_char) -> *mut c_char { + call::(request_json, api::swap_pair) +} + +#[unsafe(no_mangle)] +pub extern "C" fn amm_resolve_pool(request_json: *const c_char) -> *mut c_char { + call::(request_json, api::resolve_pool) +} + +#[unsafe(no_mangle)] +pub extern "C" fn amm_swap_plan(request_json: *const c_char) -> *mut c_char { + call::(request_json, api::swap_plan) +} + +#[unsafe(no_mangle)] +pub extern "C" fn amm_program_id(request_json: *const c_char) -> *mut c_char { + call::(request_json, api::program_id) +} + /// Releases a string returned by an `amm_*` operation. /// /// # Safety diff --git a/apps/amm/client/src/lib.rs b/apps/amm/client/src/lib.rs index 9b407f7b..b0f0b1bf 100644 --- a/apps/amm/client/src/lib.rs +++ b/apps/amm/client/src/lib.rs @@ -6,7 +6,9 @@ mod ffi; pub mod api; pub use api::{ - config_id, context, pair_ids, plan, quote, token_ids, AccountRead, AmmApiError, AmmResponse, - AmmResult, ConfigIdRequest, ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest, - PositionRequest, QuoteRequest, TokenIdsRequest, WalletAccount, NEW_POSITION_SCHEMA, + config_id, context, pair_ids, plan, program_id, quote, resolve_pool, swap_pair, swap_plan, + token_ids, AccountRead, AmmApiError, AmmResponse, AmmResult, ConfigIdRequest, ContextRequest, + PairIdsRequest, PairSnapshot, PlanRequest, PositionRequest, ProgramIdRequest, QuoteRequest, + ResolvePoolRequest, SwapPairRequest, SwapPlanRequest, TokenIdsRequest, WalletAccount, + NEW_POSITION_SCHEMA, }; diff --git a/apps/amm/flake.nix b/apps/amm/flake.nix index 5a2990dc..6b784955 100644 --- a/apps/amm/flake.nix +++ b/apps/amm/flake.nix @@ -30,17 +30,17 @@ amm_client.url = "path:../.."; }; - # NOTE: this flake is no longer built standalone. The amm_client_ffi crate + # 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_ffi via `self`. The repo-root flake exposes the UI + # 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_ffi`. + # `nix build .#amm_client`. outputs = inputs@{ logos-module-builder, shared_wallet, ... }: logos-module-builder.lib.mkLogosQmlModule { src = ./.; diff --git a/apps/amm/metadata.json b/apps/amm/metadata.json index bc2cce38..7b123c9c 100644 --- a/apps/amm/metadata.json +++ b/apps/amm/metadata.json @@ -15,7 +15,6 @@ "runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp", "libbase58"] }, "external_libraries": [ - { "name": "amm_client_ffi" }, { "name": "amm_client" } ], "cmake": { diff --git a/apps/amm/src/AmmClient.cpp b/apps/amm/src/AmmClient.cpp index 17d6d61b..c78ce181 100644 --- a/apps/amm/src/AmmClient.cpp +++ b/apps/amm/src/AmmClient.cpp @@ -69,3 +69,23 @@ AmmClientResult BundledAmmClient::plan(const QJsonObject& request) const { return call(amm_plan, request); } + +AmmClientResult BundledAmmClient::swapPair(const QJsonObject& request) const +{ + return call(amm_swap_pair, request); +} + +AmmClientResult BundledAmmClient::resolvePool(const QJsonObject& request) const +{ + return call(amm_resolve_pool, request); +} + +AmmClientResult BundledAmmClient::swapPlan(const QJsonObject& request) const +{ + return call(amm_swap_plan, request); +} + +AmmClientResult BundledAmmClient::programId(const QJsonObject& request) const +{ + return call(amm_program_id, request); +} diff --git a/apps/amm/src/AmmClient.h b/apps/amm/src/AmmClient.h index 6c69f48a..b78cc9b4 100644 --- a/apps/amm/src/AmmClient.h +++ b/apps/amm/src/AmmClient.h @@ -17,6 +17,10 @@ class AmmClient { virtual AmmClientResult context(const QJsonObject& request) const = 0; virtual AmmClientResult quote(const QJsonObject& request) const = 0; virtual AmmClientResult plan(const QJsonObject& request) const = 0; + virtual AmmClientResult swapPair(const QJsonObject& request) const = 0; + virtual AmmClientResult resolvePool(const QJsonObject& request) const = 0; + virtual AmmClientResult swapPlan(const QJsonObject& request) const = 0; + virtual AmmClientResult programId(const QJsonObject& request) const = 0; }; class BundledAmmClient final : public AmmClient { @@ -27,4 +31,8 @@ class BundledAmmClient final : public AmmClient { AmmClientResult context(const QJsonObject& request) const override; AmmClientResult quote(const QJsonObject& request) const override; AmmClientResult plan(const QJsonObject& request) const override; + AmmClientResult swapPair(const QJsonObject& request) const override; + AmmClientResult resolvePool(const QJsonObject& request) const override; + AmmClientResult swapPlan(const QJsonObject& request) const override; + AmmClientResult programId(const QJsonObject& request) const override; }; diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index acb5396e..60a3836d 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -21,37 +21,11 @@ #include "AmmClient.h" #include "LogosWalletProvider.h" #include "NewPositionRuntime.h" +#include "SwapRuntime.h" #include "WalletController.h" #include "logos_api.h" #include "logos_sdk.h" -extern "C" { -#include "amm_client_ffi.h" -} - -// Debug tracing for the swap path (resolvePool / swapExactInput), gated behind -// the AMM_DEBUG env var so normal runs stay quiet. When disabled, the streamed -// arguments — including the account_id_to_base58 round-trips used to render -// accounts — are NOT evaluated, so there's no per-call overhead. -// -// NOTE: `send_generic_public_transaction(account_ids, signing_requirements, -// instruction, program_id_hex)`. `instruction` is a byte string (`bstr`): -// declaring it as Vec makes the module's Qt/QtRO glue downgrade it to an -// opaque `any` the separate module process can't deserialize, silently dropping -// every argument. We send `instruction` as the little-endian bytes of the u32 -// words, and the deployed program by its id hex (not the raw ELF). Requires the -// wallet module built with the byte-string param; see -// docs/amm-swap-qtro-serialization-bug.md. -static bool ammDebugEnabled() -{ - static const bool on = qEnvironmentVariableIsSet("AMM_DEBUG"); - return on; -} -// AMM_DBG() << ... behaves like qWarning().noquote() but only when AMM_DEBUG is -// set; otherwise the whole statement (and its arguments) is skipped. -#define AMM_DBG() \ - if (ammDebugEnabled()) qWarning().noquote() - namespace { // Absolute path to the deployed AMM program's RISC Zero program binary // (amm.bin — the `ProgramBinary` `.bin` from the docker guest build, decoded @@ -67,78 +41,6 @@ namespace { // (see apps/amm/README.md). Config-driven so the Swap view's token picker // doesn't need a hardcoded/dummy token list. const char TOKENS_CONFIG_ENV[] = "TOKENS_CONFIG"; - - QString bytes32ToHex(const uint8_t (&b)[32]) { - return QString::fromLatin1(QByteArray(reinterpret_cast(b), 32).toHex()); - } - - bool hexToBytes32(const QString& hex, uint8_t (&out)[32]) { - const QByteArray bytes = QByteArray::fromHex(hex.toUtf8()); - if (bytes.size() != 32) - return false; - for (int i = 0; i < 32; ++i) - out[i] = static_cast(bytes[i]); - return true; - } - - // Little-endian 16-byte u128 -> decimal string. QString has no direct u128 - // constructor, so accumulate into unsigned __int128 and extract digits. - QString u128leToDecimal(const uint8_t (&le)[16]) { - unsigned __int128 value = 0; - for (int i = 15; i >= 0; --i) - value = (value << 8) | static_cast(le[i]); - - if (value == 0) - return QStringLiteral("0"); - - QString digits; - while (value > 0) { - const unsigned int digit = static_cast(value % 10); - digits.prepend(QChar(static_cast('0' + digit))); - value /= 10; - } - return digits; - } - - // Decimal string -> little-endian 16-byte u128. Inverse of u128leToDecimal. - // Returns false (leaving `out` unwritten) on an empty string or a - // non-digit character. - bool decimalToU128Le(const QString& decimal, uint8_t (&out)[16]) { - const QString trimmed = decimal.trimmed(); - if (trimmed.isEmpty()) - return false; - - const unsigned __int128 kMax = ~static_cast(0); - unsigned __int128 value = 0; - for (const QChar ch : trimmed) { - if (!ch.isDigit()) - return false; - const unsigned int d = static_cast(ch.digitValue()); - if (value > (kMax - d) / 10) - return false; // would overflow u128 - value = value * 10 + d; - } - - for (int i = 0; i < 16; ++i) { - out[i] = static_cast(value & 0xFF); - value >>= 8; - } - return true; - } - - // Base58 address of the fixed clock account consumed by SwapExactInput's - // deadline check. Resolved to hex via the wallet module rather than - // hardcoding a guessed hex encoding. - const char CLOCK_ACCOUNT_BASE58[] = "4BdcjoXkq786TMWcBGGHqcxeLYMZmn17rL4eM9ZyRWNU"; - - // Extracts the hex-encoded raw account bytes from a get_account_public() - // JSON reply. Returns an empty string if the account has no data (i.e. is - // uninitialized/nonexistent) — note the module always includes the "data" - // key, set to "" rather than omitted, when there's nothing to read. - QString accountDataHex(const QString& accountJson) { - const QJsonObject obj = QJsonDocument::fromJson(accountJson.toUtf8()).object(); - return obj.value(QStringLiteral("data")).toString(); - } } AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent) @@ -149,7 +51,8 @@ AmmUiBackend::AmmUiBackend(LogosAPI* logosAPI, QObject* parent) m_walletController(std::make_unique( *m_wallet, QStringLiteral("AmmUI"))), m_ammClient(std::make_unique()), - m_newPosition(std::make_unique(m_wallet.get(), m_ammClient.get())) + m_newPosition(std::make_unique(m_wallet.get(), m_ammClient.get())), + m_swap(std::make_unique(m_wallet.get(), m_ammClient.get())) { setWalletStateReady(false); setNewPositionContext(m_newPosition->context( @@ -291,24 +194,16 @@ QString AmmUiBackend::ammProgramIdHex() const QByteArray elf = loadAmmElf(); if (elf.isEmpty()) return QString(); - ProgramId ammId{}; - if (!amm_client_program_id_from_elf(reinterpret_cast(elf.constData()), - static_cast(elf.size()), &ammId)) { - qWarning() << "AmmUiBackend::ammProgramIdHex: amm_client_program_id_from_elf failed"; + // Hand the deployed program binary to the amm_client 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 AmmClientResult result = m_ammClient->programId( + QJsonObject { { QStringLiteral("elf"), QString::fromLatin1(elf.toHex()) } }); + if (!result.ok) { + qWarning() << "AmmUiBackend::ammProgramIdHex: amm_program_id failed"; return QString(); } - // 32 bytes, little-endian per u32 word, lowercase hex — same encoding as - // swapExactInput's program-id and `spel program-id`. - QByteArray bytes; - bytes.reserve(32); - for (int i = 0; i < 8; ++i) { - const uint32_t word = ammId[i]; - bytes.append(static_cast(word & 0xff)); - bytes.append(static_cast((word >> 8) & 0xff)); - bytes.append(static_cast((word >> 16) & 0xff)); - bytes.append(static_cast((word >> 24) & 0xff)); - } - return QString::fromLatin1(bytes.toHex()); + return result.value.value(QStringLiteral("programId")).toString(); } ActiveNetworkSnapshot AmmUiBackend::networkSnapshot() @@ -395,278 +290,19 @@ QByteArray AmmUiBackend::loadAmmElf() QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex) { - QVariantMap out; - out[QStringLiteral("exists")] = false; - - // 1. Load the deployed AMM program's ELF. We can't derive this ourselves — - // it must match whatever is actually deployed on the target sequencer. - const QByteArray elf = loadAmmElf(); - if (elf.isEmpty()) { - out[QStringLiteral("error")] = QStringLiteral("no_program_bin"); - return out; - } - - // 2. Derive the AMM program id from the ELF bytes. - ProgramId ammId{}; - if (!amm_client_program_id_from_elf(reinterpret_cast(elf.constData()), - static_cast(elf.size()), &ammId)) { - qWarning() << "AmmUiBackend::resolvePool: amm_client_program_id_from_elf failed"; - out[QStringLiteral("error")] = QStringLiteral("bad_program_bin"); - return out; - } - - // 3. Read + decode the AMM config account to discover the TWAP oracle - // program id (needed for the current-tick PDA below). No data means the - // AMM hasn't been initialized on this sequencer yet. - uint8_t configPda[32]; - amm_client_config_pda(&ammId, &configPda); - const QString configHex = bytes32ToHex(configPda); - - // Debug: log every derived account (base58 + hex) and every on-chain read so - // a failed resolve can be traced directly against `spel inspect`. - // account_id_to_base58 is a LOCAL encode (safe even when on-chain reads - // time out). - auto b58 = [this](const QString& hex) { - const QString s = m_logos->logos_execution_zone.account_id_to_base58(hex); - return s.isEmpty() ? hex : QStringLiteral("%1 (hex %2)").arg(s, hex); - }; - AMM_DBG() << "[amm-debug] resolvePool: defA=" << b58(defAHex) - << "defB=" << b58(defBHex); - AMM_DBG() << "[amm-debug] resolvePool: reading config account" << b58(configHex); - - const QString configJson = m_logos->logos_execution_zone.get_account_public(configHex); - AMM_DBG() << "[amm-debug] resolvePool: config get_account_public ->" - << configJson.size() << "chars; raw:" << configJson.left(400); - const QString configDataHex = accountDataHex(configJson); - if (configDataHex.isEmpty()) { - qWarning() << "AmmUiBackend::resolvePool: AMM config read returned no data" - << "(a failed/timed-out RPC or an uninitialized account)"; - out[QStringLiteral("error")] = QStringLiteral("amm_not_initialized"); - return out; - } - - const QByteArray configBytes = QByteArray::fromHex(configDataHex.toUtf8()); - FfiConfigView configView{}; - if (!amm_client_decode_config(reinterpret_cast(configBytes.constData()), - static_cast(configBytes.size()), &configView)) { - qWarning() << "AmmUiBackend::resolvePool: amm_client_decode_config failed"; - out[QStringLiteral("error")] = QStringLiteral("bad_config"); - return out; - } - - // 4. Derive the pool PDA and read + decode its account. No data means - // there's no pool (or no liquidity) for this token pair yet. - uint8_t defA[32]; - uint8_t defB[32]; - if (!hexToBytes32(defAHex, defA) || !hexToBytes32(defBHex, defB)) { - qWarning() << "AmmUiBackend::resolvePool: invalid defAHex/defBHex"; - out[QStringLiteral("error")] = QStringLiteral("bad_config"); - return out; - } - - uint8_t poolPda[32]; - amm_client_pool_pda(&ammId, &defA, &defB, &poolPda); - const QString poolHex = bytes32ToHex(poolPda); - - AMM_DBG() << "[amm-debug] resolvePool: reading pool account" << b58(poolHex); - const QString poolJson = m_logos->logos_execution_zone.get_account_public(poolHex); - AMM_DBG() << "[amm-debug] resolvePool: pool get_account_public ->" - << poolJson.size() << "chars; raw:" << poolJson.left(400); - const QString poolDataHex = accountDataHex(poolJson); - if (poolDataHex.isEmpty()) { - // Not a warning: this is the normal "no liquidity yet" state. - out[QStringLiteral("error")] = QStringLiteral("no_pool"); - return out; - } - - const QByteArray poolBytes = QByteArray::fromHex(poolDataHex.toUtf8()); - FfiPoolView poolView{}; - if (!amm_client_decode_pool(reinterpret_cast(poolBytes.constData()), - static_cast(poolBytes.size()), &poolView)) { - qWarning() << "AmmUiBackend::resolvePool: amm_client_decode_pool failed"; - out[QStringLiteral("error")] = QStringLiteral("bad_config"); - return out; - } - - // 5. Derive the TWAP oracle's current-tick PDA for this pool. - uint8_t currentTickPda[32]; - amm_client_current_tick_pda(&configView.twap_oracle_program_id, &poolPda, ¤tTickPda); - - // 6. Assemble the result. - out[QStringLiteral("exists")] = true; - out[QStringLiteral("configHex")] = configHex; - out[QStringLiteral("poolIdHex")] = poolHex; - out[QStringLiteral("defAHex")] = bytes32ToHex(poolView.def_a); - out[QStringLiteral("defBHex")] = bytes32ToHex(poolView.def_b); - out[QStringLiteral("vaultAHex")] = bytes32ToHex(poolView.vault_a); - out[QStringLiteral("vaultBHex")] = bytes32ToHex(poolView.vault_b); - out[QStringLiteral("currentTickHex")] = bytes32ToHex(currentTickPda); - out[QStringLiteral("reserveA")] = u128leToDecimal(poolView.reserve_a); - out[QStringLiteral("reserveB")] = u128leToDecimal(poolView.reserve_b); - out[QStringLiteral("feeBps")] = static_cast(poolView.fees); - - AMM_DBG() << "[amm-debug] resolvePool: RESOLVED" - << "pool=" << b58(poolHex) - << "poolDefA=" << b58(out[QStringLiteral("defAHex")].toString()) - << "poolDefB=" << b58(out[QStringLiteral("defBHex")].toString()) - << "vaultA=" << b58(out[QStringLiteral("vaultAHex")].toString()) - << "vaultB=" << b58(out[QStringLiteral("vaultBHex")].toString()) - << "currentTick=" << b58(out[QStringLiteral("currentTickHex")].toString()) - << "reserveA=" << out[QStringLiteral("reserveA")].toString() - << "reserveB=" << out[QStringLiteral("reserveB")].toString() - << "feeBps=" << out[QStringLiteral("feeBps")].toInt(); - return out; + return m_swap->resolvePool(defAHex, defBHex, networkSnapshot()); } QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex, QString userOutputHoldingHex, QString amountInDecimal, QString minOutDecimal, QString deadlineDecimal) { - AMM_DBG() << "[amm-debug] swapExactInput: ARGS" - << "defA=" << defAHex << "defB=" << defBHex - << "userInputHolding=" << userInputHoldingHex - << "userOutputHolding=" << userOutputHoldingHex - << "amountIn=" << amountInDecimal << "minOut=" << minOutDecimal - << "deadline=" << deadlineDecimal; - - // 1. Resolve the pool's PDAs; refuse if there's no pool (or no liquidity) - // for this token pair. - const QVariantMap pool = resolvePool(defAHex, defBHex); - if (!pool.value(QStringLiteral("exists")).toBool()) { - qWarning() << "AmmUiBackend::swapExactInput: no pool for the given token pair"; - return QString(); - } - - // 2. Load the deployed AMM program's ELF (must match resolvePool's — both - // read the same AMM_PROGRAM_BIN — since the instruction is proven against - // this exact binary's image id). - const QByteArray elf = loadAmmElf(); - if (elf.isEmpty()) { - qWarning() << "AmmUiBackend::swapExactInput: failed to load AMM_PROGRAM_BIN"; - return QString(); - } - - // 3. Convert amounts/deadline to the wire types amm_client_swap_words expects. - uint8_t amtIn[16]; - uint8_t minOut[16]; - if (!decimalToU128Le(amountInDecimal, amtIn) || !decimalToU128Le(minOutDecimal, minOut)) { - qWarning() << "AmmUiBackend::swapExactInput: invalid amountInDecimal/minOutDecimal"; - return QString(); - } - bool deadlineOk = false; - const quint64 deadline = deadlineDecimal.toULongLong(&deadlineOk); - if (!deadlineOk) { - qWarning() << "AmmUiBackend::swapExactInput: invalid deadlineDecimal"; - return QString(); - } - - // 4. Build the RISC0 instruction words for SwapExactInput. - const AmmWords w = amm_client_swap_words(&amtIn, &minOut, deadline); - if (!w.ok) { - qWarning() << "AmmUiBackend::swapExactInput: amm_client_swap_words failed"; - return QString(); - } - const std::vector instruction(w.ptr, w.ptr + w.len); - amm_client_free_words(w); - - // 5. Assemble the account id list (exact IDL order) and parallel signer - // flags. The swap's direction is derived from the input holding's own - // token, so the two user holdings occupy fixed role slots: user_input_holding - // (the token being sold) then user_output_holding (received). Only the input - // holding signs — the guest debits it via the downstream token transfer; the - // output holding only receives and needs no signature. - const QString clockHex = - m_logos->logos_execution_zone.account_id_from_base58(QString::fromLatin1(CLOCK_ACCOUNT_BASE58)); - if (clockHex.isEmpty()) { - qWarning() << "AmmUiBackend::swapExactInput: failed to resolve clock account id"; - return QString(); - } - - const QStringList accounts = { - pool.value(QStringLiteral("configHex")).toString(), - pool.value(QStringLiteral("poolIdHex")).toString(), - pool.value(QStringLiteral("vaultAHex")).toString(), - pool.value(QStringLiteral("vaultBHex")).toString(), - userInputHoldingHex, // user_input_holding — the token being sold (signed) - userOutputHoldingHex, // user_output_holding — receives the token being bought - pool.value(QStringLiteral("currentTickHex")).toString(), - clockHex, - }; - const QVariantList signers = { false, false, false, false, true, false, false, false }; - - // Debug: dump the exact accounts/signers we submit (base58 for `spel` - // comparison), plus the instruction/elf sizes. - auto b58s = [this](const QString& hex) { - const QString s = m_logos->logos_execution_zone.account_id_to_base58(hex); - return s.isEmpty() ? hex : QStringLiteral("%1 (hex %2)").arg(s, hex); - }; - static const char* const kSlot[] = { "config", "pool", - "vault_a", "vault_b", - "user_input_holding", "user_output_holding", - "current_tick", "clock" }; - AMM_DBG() << "[amm-debug] swapExactInput: SUBMIT" - << "instruction_words=" << static_cast(instruction.size()) - << "elf_bytes=" << elf.size(); - for (int i = 0; i < accounts.size(); ++i) { - AMM_DBG() << "[amm-debug] account[" << i << "]" - << (i < 8 ? kSlot[i] : "?") << "=" << b58s(accounts[i]) - << "signer=" << (i < signers.size() && signers[i].toBool()); - } - - // 6. Submit through the wallet module's generic public-transaction entry - // point. The module API takes `instruction` (as bytes — see the note atop - // this file) plus the program's id as hex, not the raw ELF: the program is - // already deployed and referenced by id. There is no program_dependencies - // arg — the sequencer resolves the AMM's chained token/twap calls on-chain. - // Serialize the u32 instruction words to bytes explicitly as little-endian, - // matching the protocol's LE-u32 wire format. A reinterpret_cast of the - // in-memory vector would be host-endian-dependent and byte-swap on a - // big-endian host, making the guest decode a different instruction. - QByteArray instructionBytes; - instructionBytes.reserve(static_cast(instruction.size() * sizeof(uint32_t))); - for (const uint32_t w : instruction) { - instructionBytes.append(static_cast(w & 0xFF)); - instructionBytes.append(static_cast((w >> 8) & 0xFF)); - instructionBytes.append(static_cast((w >> 16) & 0xFF)); - instructionBytes.append(static_cast((w >> 24) & 0xFF)); - } - - // Derive the deployed AMM program id from the ELF and hex-encode it as the - // canonical 32-byte hex (little-endian per u32 word — matches `spel - // program-id` and the on-chain config's *_program_id fields). - ProgramId ammId; - if (!amm_client_program_id_from_elf(reinterpret_cast(elf.constData()), - static_cast(elf.size()), &ammId)) { - qWarning() << "AmmUiBackend::swapExactInput: amm_client_program_id_from_elf failed"; - return QString(); - } - QByteArray programIdBytes; - programIdBytes.reserve(32); - for (int i = 0; i < 8; ++i) { - const uint32_t w = ammId[i]; - programIdBytes.append(static_cast(w & 0xff)); - programIdBytes.append(static_cast((w >> 8) & 0xff)); - programIdBytes.append(static_cast((w >> 16) & 0xff)); - programIdBytes.append(static_cast((w >> 24) & 0xff)); - } - const QString programIdHex = QString::fromLatin1(programIdBytes.toHex()); - AMM_DBG() << "[amm-debug] swapExactInput: program_id_hex=" << programIdHex; - - // `instruction` is a QVariant (bstr); `program_id_hex` is a plain QString - // (tstr) in the generated proxy — pass it directly, not QVariant-wrapped. - const QString resultJson = m_logos->logos_execution_zone.send_generic_public_transaction( - accounts, signers, QVariant::fromValue(instructionBytes), programIdHex); - AMM_DBG() << "[amm-debug] swapExactInput: send_generic_public_transaction ->" - << resultJson.size() << "chars; raw:" << resultJson.left(600); - - const QJsonObject obj = QJsonDocument::fromJson(resultJson.toUtf8()).object(); - if (!obj.value(QStringLiteral("success")).toBool()) { - qWarning() << "AmmUiBackend::swapExactInput: transaction failed:" << resultJson; - return QString(); - } - - refreshBalances(); - return obj.value(QStringLiteral("tx_hash")).toString(); + const QString txHash = m_swap->swap(defAHex, defBHex, userInputHoldingHex, userOutputHoldingHex, + amountInDecimal, minOutDecimal, deadlineDecimal, + networkSnapshot(), isWalletOpen()); + if (!txHash.isEmpty()) + refreshBalances(); + return txHash; } QVariantList AmmUiBackend::tokenList() diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index 854a105b..005d4726 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -15,15 +15,12 @@ #include "ActiveNetwork.h" #include "WalletAccountModel.h" -extern "C" { -#include "amm_client_ffi.h" -} - class LogosAPI; struct LogosModules; class AmmClient; class LogosWalletProvider; class NewPositionRuntime; +class SwapRuntime; class WalletController; // Source-side implementation of the AmmUiBackend .rep interface. @@ -100,6 +97,7 @@ public slots: std::unique_ptr m_walletController; std::unique_ptr m_ammClient; std::unique_ptr m_newPosition; + std::unique_ptr m_swap; QVariantMap m_newPositionHints; diff --git a/apps/amm/src/SwapRuntime.cpp b/apps/amm/src/SwapRuntime.cpp new file mode 100644 index 00000000..ea423d24 --- /dev/null +++ b/apps/amm/src/SwapRuntime.cpp @@ -0,0 +1,145 @@ +#include "SwapRuntime.h" + +#include +#include +#include + +#include "AmmClient.h" +#include "WalletProvider.h" + +namespace { + QJsonObject accountReadJson(const WalletAccountRead& read) + { + QJsonObject result { + { QStringLiteral("id"), read.accountId }, + { QStringLiteral("status"), read.status }, + }; + if (read.ok()) { + result.insert(QStringLiteral("account"), QJsonObject { + { QStringLiteral("program_owner"), read.programOwner }, + { QStringLiteral("balance"), read.balanceHex }, + { QStringLiteral("nonce"), read.nonceHex }, + { QStringLiteral("data"), read.dataHex }, + }); + } + return result; + } + + QStringList jsonStringList(const QJsonArray& values) + { + QStringList result; + result.reserve(values.size()); + for (const QJsonValue& value : values) + result.append(value.toString()); + return result; + } + + QVector jsonBoolList(const QJsonArray& values) + { + QVector result; + result.reserve(values.size()); + for (const QJsonValue& value : values) + result.append(value.toBool()); + return result; + } + + QVector jsonUIntList(const QJsonArray& values) + { + QVector result; + result.reserve(values.size()); + for (const QJsonValue& value : values) + result.append(static_cast(value.toInteger())); + return result; + } +} + +SwapRuntime::SwapRuntime(WalletProvider* wallet, AmmClient* client) + : m_wallet(wallet), + m_client(client) +{ +} + +QJsonObject SwapRuntime::readConfig(const ActiveNetworkSnapshot& network) const +{ + const AmmClientResult configResult = m_client->configId( + QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId } }); + if (!configResult.ok) + return {}; + return accountReadJson(m_wallet->readPublicAccount( + configResult.value.value(QStringLiteral("configId")).toString())); +} + +QVariantMap SwapRuntime::resolvePool(const QString& tokenInId, + const QString& tokenOutId, + const ActiveNetworkSnapshot& network) +{ + const QVariantMap absent { { QStringLiteral("exists"), false } }; + if (network.status != QStringLiteral("ready")) + return absent; + + const QJsonObject config = readConfig(network); + if (config.isEmpty()) + return absent; + + const AmmClientResult pairResult = m_client->swapPair(QJsonObject { + { QStringLiteral("ammProgramId"), network.ammProgramId }, + { QStringLiteral("tokenInId"), tokenInId }, + { QStringLiteral("tokenOutId"), tokenOutId }, + { QStringLiteral("config"), config }, + }); + if (!pairResult.ok + || pairResult.value.value(QStringLiteral("status")).toString() != QStringLiteral("ok")) + return absent; + + const QJsonObject pool = accountReadJson(m_wallet->readPublicAccount( + pairResult.value.value(QStringLiteral("poolId")).toString())); + const AmmClientResult resolveResult = + m_client->resolvePool(QJsonObject { { QStringLiteral("pool"), pool } }); + if (!resolveResult.ok) + return absent; + return resolveResult.value.toVariantMap(); +} + +QString SwapRuntime::swap(const QString& tokenInId, + const QString& tokenOutId, + const QString& userInputHoldingId, + const QString& userOutputHoldingId, + const QString& amountInDecimal, + const QString& minOutDecimal, + const QString& deadlineMs, + const ActiveNetworkSnapshot& network, + bool walletOpen) +{ + if (network.status != QStringLiteral("ready") || !walletOpen) + return {}; + + const QJsonObject config = readConfig(network); + if (config.isEmpty()) + return {}; + + const AmmClientResult planResult = m_client->swapPlan(QJsonObject { + { QStringLiteral("ammProgramId"), network.ammProgramId }, + { QStringLiteral("tokenInId"), tokenInId }, + { QStringLiteral("tokenOutId"), tokenOutId }, + { QStringLiteral("config"), config }, + { QStringLiteral("userInputHoldingId"), userInputHoldingId }, + { QStringLiteral("userOutputHoldingId"), userOutputHoldingId }, + { QStringLiteral("amountIn"), amountInDecimal }, + { QStringLiteral("minOut"), minOutDecimal }, + { QStringLiteral("deadlineMs"), deadlineMs }, + }); + if (!planResult.ok + || planResult.value.value(QStringLiteral("status")).toString() != QStringLiteral("ready")) + return {}; + + const QJsonObject plan = planResult.value; + const WalletSubmission submission = m_wallet->submitPublicTransaction({ + plan.value(QStringLiteral("programId")).toString(), + jsonStringList(plan.value(QStringLiteral("accountIds")).toArray()), + jsonBoolList(plan.value(QStringLiteral("signingRequirements")).toArray()), + jsonUIntList(plan.value(QStringLiteral("instruction")).toArray()), + }); + if (!submission.accepted()) + return {}; + return submission.nativeHash; +} diff --git a/apps/amm/src/SwapRuntime.h b/apps/amm/src/SwapRuntime.h new file mode 100644 index 00000000..4bf04f5a --- /dev/null +++ b/apps/amm/src/SwapRuntime.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include "ActiveNetwork.h" + +class AmmClient; +class WalletProvider; + +// Off-chain orchestration for the Swap view: reads accounts through the wallet, +// drives the amm_client swap ops, and submits the swap transaction. The AMM +// domain logic lives in the amm_client crate; this class only glues account +// reads and submission to it. Mirrors NewPositionRuntime. +class SwapRuntime { +public: + SwapRuntime(WalletProvider* wallet, AmmClient* client); + + // { exists, reserveA, reserveB, feeBps } for the (tokenIn, tokenOut) pool. + // reserveA/reserveB follow the pool's canonical def order (the caller maps + // them to sell/buy). exists=false when the pool is absent or has no liquidity. + QVariantMap resolvePool(const QString& tokenInId, + const QString& tokenOutId, + const ActiveNetworkSnapshot& network); + + // Builds and submits a SwapExactInput transaction; returns the native tx + // hash on success, an empty string on any failure. + QString swap(const QString& tokenInId, + const QString& tokenOutId, + const QString& userInputHoldingId, + const QString& userOutputHoldingId, + const QString& amountInDecimal, + const QString& minOutDecimal, + const QString& deadlineMs, + const ActiveNetworkSnapshot& network, + bool walletOpen); + +private: + // Derives the config account id (config_id) and reads it. Returns an empty + // object only when the config_id op itself fails. + QJsonObject readConfig(const ActiveNetworkSnapshot& network) const; + + WalletProvider* m_wallet; + AmmClient* m_client; +}; diff --git a/flake.nix b/flake.nix index 61f36614..1fa131a8 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ }; # The AMM QML UI module (apps/amm) is built from this same flake so it can - # reference the amm_client_ffi crate package via `self` — no filesystem + # reference the amm_client 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_ffi`. + # twap_oracle_core, token_core, ...) to resolve `-p amm_client`. src = ./.; commonArgs = { inherit src; strictDeps = true; - pname = "amm_client_ffi"; + pname = "amm_client"; 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 + # WOULD invoke Metal on darwin). `-p amm_client` never builds # those crates or their build scripts. - cargoExtraArgs = "-p amm_client_ffi"; + cargoExtraArgs = "-p amm_client"; 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,14 +76,17 @@ cargoArtifacts = craneLib.buildDepsOnly commonArgs; - ammClientFfi = craneLib.buildPackage ( + # The single AMM host FFI crate (apps/amm/client): 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 ( commonArgs // { inherit cargoArtifacts; postInstall = '' mkdir -p $out/include - cp programs/amm/client-ffi/amm_client_ffi.h $out/include/ + cp apps/amm/client/include/amm_client.h $out/include/ '' + pkgs.lib.optionalString pkgs.stdenv.isDarwin '' # Set the dylib's install-name to its ABSOLUTE store path (NOT @@ -92,31 +95,6 @@ # @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_ffi.dylib ]; then - install_name_tool -id "$out/lib/libamm_client_ffi.dylib" $out/lib/libamm_client_ffi.dylib - fi - ''; - } - ); - - # Second AMM client crate (apps/amm/client) — the new-position/pool - # flow's protocol lib. Built alongside amm_client_ffi (they coexist: - # symbols are prefixed amm_* vs amm_client_*). TODO: consolidate the two - # into a single AMM client FFI. - ammClientArgs = commonArgs // { - pname = "amm_client"; - cargoExtraArgs = "-p amm_client"; - }; - ammClient = craneLib.buildPackage ( - ammClientArgs - // { - cargoArtifacts = craneLib.buildDepsOnly ammClientArgs; - postInstall = - '' - mkdir -p $out/include - cp apps/amm/client/include/amm_client.h $out/include/ - '' - + pkgs.lib.optionalString pkgs.stdenv.isDarwin '' if [ -f $out/lib/libamm_client.dylib ]; then install_name_tool -id "$out/lib/libamm_client.dylib" $out/lib/libamm_client.dylib fi @@ -125,14 +103,13 @@ ); in { - packages.default = ammClientFfi; - packages.amm_client_ffi = ammClientFfi; + packages.default = ammClient; packages.amm_client = ammClient; } ); # The AMM QML UI module (apps/amm). Its external_libraries entry - # (amm_client_ffi) is resolved to `self.packages.${system}.amm_client_ffi` + # (amm_client) is resolved to `self.packages.${system}.amm_client` # above — the module builder's resolveExtInput reads # `flakeInput.packages.${system}.${pkgName}`, and passing `self` here # works because `self` is this very flake, which already exposes that @@ -142,7 +119,6 @@ configFile = ./apps/amm/metadata.json; flakeInputs = inputs; externalLibInputs = { - amm_client_ffi = { input = self; packages.default = "amm_client_ffi"; }; amm_client = { input = self; packages.default = "amm_client"; }; }; # The AMM UI links the shared C++ wallet access lib and bundles the @@ -178,20 +154,19 @@ appPkgs = appOutputs.packages or { }; # Wrap the app launcher to export DYLD_FALLBACK_LIBRARY_PATH pointing at the - # amm_client_ffi lib. The logos module builder links the plugin against - # @rpath/libamm_client_ffi.dylib but does NOT stage that dylib into the + # amm_client lib. The logos module builder links the plugin against + # @rpath/libamm_client.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 ]; }; - ammFfi = crateOutputs.packages.${system}.amm_client_ffi; ammClient = crateOutputs.packages.${system}.amm_client; in app // { program = "${pkgs.writeShellScript "run-amm-ui" '' - export DYLD_FALLBACK_LIBRARY_PATH="${ammFfi}/lib:${ammClient}/lib''${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" + export DYLD_FALLBACK_LIBRARY_PATH="${ammClient}/lib''${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" exec ${app.program} "$@" ''}"; }; diff --git a/programs/amm/client-ffi/Cargo.toml b/programs/amm/client-ffi/Cargo.toml deleted file mode 100644 index cd915d35..00000000 --- a/programs/amm/client-ffi/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "amm_client_ffi" -version = "0.1.0" -# 2021 (not the repo's 2024): matches amm_core and keeps plain #[no_mangle] -# (edition 2024 requires #[unsafe(no_mangle)]). -edition = "2021" - -[lib] -name = "amm_client_ffi" -# The logos module builder's macOS find_library only locates SHARED libs -# (lib.dylib), never a static .a — so we must ship the cdylib. The flake -# gives the dylib an ABSOLUTE store-path install-name (not @rpath), so the plugin -# links it by its /nix/store path (kept in the closure) and dlopen resolves it at -# runtime without any rpath staging. staticlib/rlib kept for other consumers/tests. -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", 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 deleted file mode 100644 index 7f4d87c5..00000000 --- a/programs/amm/client-ffi/amm_client_ffi.h +++ /dev/null @@ -1,155 +0,0 @@ -#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; - -/** - * C-ABI mirror of `pool::ConfigView`. - */ -typedef struct FfiConfigView { - uint32_t token_program_id[8]; - uint32_t twap_oracle_program_id[8]; - uint8_t authority[32]; - bool ok; -} FfiConfigView; - -/** - * 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); - -/** - * Decodes an `AmmConfig` 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 `FfiConfigView`. - */ -bool amm_client_decode_config(const uint8_t *bytes, uintptr_t len, struct FfiConfigView *out); - -#endif /* AMM_CLIENT_FFI_H */ diff --git a/programs/amm/client-ffi/src/lib.rs b/programs/amm/client-ffi/src/lib.rs deleted file mode 100644 index c9ff4426..00000000 --- a/programs/amm/client-ffi/src/lib.rs +++ /dev/null @@ -1,244 +0,0 @@ -//! 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, -} - -/// C-ABI mirror of `pool::ConfigView`. -#[repr(C)] -pub struct FfiConfigView { - pub token_program_id: [u32; 8], - pub twap_oracle_program_id: [u32; 8], - pub authority: [u8; 32], - 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 deployed program binary — the RISC -/// Zero `ProgramBinary` (`.bin`) format, NOT a raw ELF (the `elf` bytes are -/// decoded via `ProgramBinary::decode`). Returns `false` on invalid input, -/// 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, - } -} - -/// Decodes an `AmmConfig` 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 `FfiConfigView`. -#[no_mangle] -pub unsafe extern "C" fn amm_client_decode_config( - bytes: *const u8, - len: usize, - out: *mut FfiConfigView, -) -> bool { - let b = core::slice::from_raw_parts(bytes, len); - match pool::decode_config(b) { - Ok(v) => { - *out = FfiConfigView { - token_program_id: v.token_program_id, - twap_oracle_program_id: v.twap_oracle_program_id, - authority: v.authority, - ok: true, - }; - true - } - Err(_) => false, - } -} diff --git a/programs/amm/client-ffi/src/pda.rs b/programs/amm/client-ffi/src/pda.rs deleted file mode 100644 index a1bb0936..00000000 --- a/programs/amm/client-ffi/src/pda.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! 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 deployed program binary — the RISC -/// Zero `ProgramBinary` (`.bin`) format produced by the guest build, NOT a raw -/// ELF (the `elf` bytes are decoded via `ProgramBinary::decode`) — 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, 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 deleted file mode 100644 index 6cfde380..00000000 --- a/programs/amm/client-ffi/src/pool.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! 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::{AmmConfig, 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).map_err(|_| format!("pool fee {} exceeds u32::MAX", p.fees))?, - }) -} - -pub struct ConfigView { - pub token_program_id: [u32; 8], - pub twap_oracle_program_id: [u32; 8], - pub authority: [u8; 32], -} - -pub fn decode_config(bytes: &[u8]) -> Result { - let c: AmmConfig = borsh::from_slice(bytes).map_err(|e| format!("{e:?}"))?; - Ok(ConfigView { - token_program_id: c.token_program_id, - twap_oracle_program_id: c.twap_oracle_program_id, - authority: c.authority.into_value(), - }) -} - -#[cfg(test)] -mod tests { - use amm_core::PoolDefinition; - use nssa_core::account::AccountId; - - 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); - } - - #[test] - fn decode_config_roundtrip() { - let c = AmmConfig { - token_program_id: [7u32; 8], - twap_oracle_program_id: [9u32; 8], - authority: AccountId::new([0u8; 32]), - }; - let bytes = borsh::to_vec(&c).unwrap(); - let v = decode_config(&bytes).unwrap(); - assert_eq!(v.token_program_id, [7u32; 8]); - assert_eq!(v.twap_oracle_program_id, [9u32; 8]); - assert_eq!(v.authority, [0u8; 32]); - } -} diff --git a/programs/amm/client-ffi/src/swap.rs b/programs/amm/client-ffi/src/swap.rs deleted file mode 100644 index 81fab768..00000000 --- a/programs/amm/client-ffi/src/swap.rs +++ /dev/null @@ -1,52 +0,0 @@ -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"), - } - } -}