feat(modules/amm): add pool_id operation

Derives a pool's PDA from the AMM program id and the two token ids —
compute_pool_pda(amm_program, canonical(token_in, token_out)) — returning
{ poolId }. Tokens may be given in either order; the op canonicalizes.

Unlike swap_pair, which derives the whole pair account-set (including the
current-tick PDA, which depends on config.twap_oracle_program_id and so
requires reading the config account), the pool address depends only on the
program id and the token pair. So a caller that just needs to locate and
read the pool can skip the config read entirely.

Exposed as the amm_pool_id C ABI export. Additive — no existing op changes.
It backs the config-free pool lookup the upcoming swap-quote path needs (and,
later, resolvePoolAccount).
This commit is contained in:
r4bbit
2026-08-05 20:24:24 +02:00
parent de9a3d5320
commit 6a951f3cad
6 changed files with 86 additions and 11 deletions
+2
View File
@@ -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);
+8 -3
View File
@@ -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)
+8
View File
@@ -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 {
+56 -2
View File
@@ -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<Value, String>
}))
}
/// 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<Value, String> {
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());
}
}
+7 -2
View File
@@ -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::<ResolvePoolRequest>(request_json, api::resolve_pool)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_pool_id(request_json: *const c_char) -> *mut c_char {
call::<PoolIdRequest>(request_json, api::pool_id)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_swap_plan(request_json: *const c_char) -> *mut c_char {
call::<SwapPlanRequest>(request_json, api::swap_plan)
+5 -4
View File
@@ -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,
};