diff --git a/modules/amm/ffi/include/amm_ffi.h b/modules/amm/ffi/include/amm_ffi.h index 69684a6..e29c1ce 100644 --- a/modules/amm/ffi/include/amm_ffi.h +++ b/modules/amm/ffi/include/amm_ffi.h @@ -30,6 +30,8 @@ char *amm_swap_pair(const char *request_json); char *amm_resolve_pool(const char *request_json); +char *amm_pool_id(const char *request_json); + char *amm_swap_plan(const char *request_json); char *amm_program_id(const char *request_json); diff --git a/modules/amm/ffi/src/api/mod.rs b/modules/amm/ffi/src/api/mod.rs index dc9ba54..e8f67d5 100644 --- a/modules/amm/ffi/src/api/mod.rs +++ b/modules/amm/ffi/src/api/mod.rs @@ -21,9 +21,9 @@ mod tests; use std::{error::Error, fmt}; pub use request::{ - ConfigIdRequest, ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest, PositionRequest, - ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapPairRequest, SwapPlanRequest, - TokenIdsRequest, + ConfigIdRequest, ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest, PoolIdRequest, + PositionRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapPairRequest, + SwapPlanRequest, TokenIdsRequest, }; use serde_json::Value; @@ -103,6 +103,11 @@ pub fn resolve_pool(request: ResolvePoolRequest) -> AmmResult { swap::resolve_pool(request).map_err(Into::into) } +/// Derives the pool PDA for a pair — config-free, so a reader needn't load config. +pub fn pool_id(request: PoolIdRequest) -> AmmResult { + swap::pool_id(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) diff --git a/modules/amm/ffi/src/api/request.rs b/modules/amm/ffi/src/api/request.rs index 698aafb..ae19112 100644 --- a/modules/amm/ffi/src/api/request.rs +++ b/modules/amm/ffi/src/api/request.rs @@ -67,6 +67,14 @@ pub struct ResolvePoolRequest { pub pool: AccountRead, } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PoolIdRequest { + pub amm_program_id: String, + pub token_in_id: String, + pub token_out_id: String, +} + #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SwapPlanRequest { diff --git a/modules/amm/ffi/src/api/swap.rs b/modules/amm/ffi/src/api/swap.rs index 84c81ab..c4d5646 100644 --- a/modules/amm/ffi/src/api/swap.rs +++ b/modules/amm/ffi/src/api/swap.rs @@ -3,14 +3,14 @@ //! 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 amm_core::{compute_pool_pda, 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, + PoolIdRequest, ProgramIdRequest, ResolvePoolRequest, SwapPairRequest, SwapPlanRequest, }; use crate::account::{ account_id_from_hex, account_id_hex, decode_account, parse_program_id, program_id_bytes, @@ -94,6 +94,21 @@ pub(super) fn resolve_pool(request: ResolvePoolRequest) -> Result })) } +/// Derives the pool PDA for a swap pair (tokens in either order). Config-free — +/// the pool address depends only on the AMM program id and the two token ids, so +/// a caller that just needs to read the pool doesn't have to load the config +/// first (unlike `swap_pair`, which also derives the config-dependent tick PDA). +pub(super) fn pool_id(request: PoolIdRequest) -> 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("pool_id requires two distinct tokens")); + } + let (token_a, token_b) = canonical_pair(token_in, token_out); + Ok(json!({ "poolId": account_id_hex(compute_pool_pda(amm_program, token_a, token_b)) })) +} + /// 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 @@ -265,4 +280,43 @@ mod tests { .unwrap(); assert_eq!(plan, expected); } + + #[test] + fn pool_id_is_order_independent_and_matches_core() { + let program = "00".repeat(32); + let a = AccountId::new([0xCC; 32]); + let b = AccountId::new([0xDD; 32]); + + let ab = pool_id(PoolIdRequest { + amm_program_id: program.clone(), + token_in_id: account_id_hex(a), + token_out_id: account_id_hex(b), + }) + .unwrap(); + let ba = pool_id(PoolIdRequest { + amm_program_id: program.clone(), + token_in_id: account_id_hex(b), + token_out_id: account_id_hex(a), + }) + .unwrap(); + // Canonical ordering makes the pool id independent of swap direction. + assert_eq!(ab, ba); + + // And it matches amm_core's PDA for the canonical pair. + let amm = parse_program_id(&program).unwrap(); + let (ca, cb) = if is_canonical_pair(a, b) { + (a, b) + } else { + (b, a) + }; + assert_eq!(ab["poolId"], account_id_hex(compute_pool_pda(amm, ca, cb))); + + // Same token in/out is rejected. + assert!(pool_id(PoolIdRequest { + amm_program_id: program, + token_in_id: account_id_hex(a), + token_out_id: account_id_hex(a), + }) + .is_err()); + } } diff --git a/modules/amm/ffi/src/ffi.rs b/modules/amm/ffi/src/ffi.rs index bd3ea26..0667635 100644 --- a/modules/amm/ffi/src/ffi.rs +++ b/modules/amm/ffi/src/ffi.rs @@ -7,8 +7,8 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::api::{ self, AmmApiError, AmmResult, ConfigIdRequest, ContextRequest, PairIdsRequest, PlanRequest, - ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapPairRequest, SwapPlanRequest, - TokenIdsRequest, + PoolIdRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapPairRequest, + SwapPlanRequest, TokenIdsRequest, }; #[derive(Serialize)] @@ -117,6 +117,11 @@ 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_pool_id(request_json: *const c_char) -> *mut c_char { + call::(request_json, api::pool_id) +} + #[unsafe(no_mangle)] pub extern "C" fn amm_swap_plan(request_json: *const c_char) -> *mut c_char { call::(request_json, api::swap_plan) diff --git a/modules/amm/ffi/src/lib.rs b/modules/amm/ffi/src/lib.rs index d9eb986..8e2d32f 100644 --- a/modules/amm/ffi/src/lib.rs +++ b/modules/amm/ffi/src/lib.rs @@ -6,8 +6,9 @@ mod ffi; pub mod api; pub use api::{ - 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, + config_id, context, pair_ids, plan, pool_id, program_id, quote, resolve_pool, swap_pair, + swap_plan, token_ids, AccountRead, AmmApiError, AmmResponse, AmmResult, ConfigIdRequest, + ContextRequest, PairIdsRequest, PairSnapshot, PlanRequest, PoolIdRequest, PositionRequest, + ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapPairRequest, SwapPlanRequest, + TokenIdsRequest, WalletAccount, };