feat(amm): expose supported fee tiers via feeTiers() op

The liquidity form's fee-tier selector was fed from the module's
newPositionContext, which hardcoded an empty list — leaving the selector
blank. Source the tiers from the program instead so the UI can never
drift from what the guest accepts.

Add amm_core::SUPPORTED_FEE_TIERS: the canonical ascending list of raw
bps ([1, 5, 30, 100]), built from the existing FEE_TIER_BPS_* constants.
is_supported_fee_tier's match is left unchanged and the new const is
unused on-chain, so the guest ImageID is unaffected; a drift-guard test
locks the list to the check (every entry accepted, neighbours rejected,
ascending/deduped).

Wire it through the stack:
- FFI: amm_fee_tiers op reading SUPPORTED_FEE_TIERS -> { feeTiers: [...] }
  (empty FeeTiersRequest, cbindgen header regenerated).
- Module: LogosList feeTiers() unwrapping the list, like tokenHoldings.
- Backend: QVariantList feeTiers() QtRO slot forwarding to the module.
- QML: LiquidityPage fetches backend.feeTiers() once (wallet-independent)
  and injects it into NewPositionForm, which wraps each int into a
  { feeBps } row for the existing delegate. Drop the now-dead feeTiers
  key from the flow's loadingContext().
This commit is contained in:
r4bbit
2026-08-13 14:20:39 +02:00
parent 4cfc03a815
commit 55ba1d07fe
15 changed files with 169 additions and 21 deletions
+2
View File
@@ -54,6 +54,8 @@ char *amm_token_holdings(const char *request_json);
char *amm_program_id(const char *request_json);
char *amm_fee_tiers(const char *request_json);
/**
* Releases a string returned by an `amm_*` operation.
*
+43
View File
@@ -0,0 +1,43 @@
//! Supported fee tiers. The single source of truth is `amm_core::SUPPORTED_FEE_TIERS` — the guest
//! enforces the same set via `is_supported_fee_tier`, so exposing the list here keeps the UI from
//! drifting. Raw basis points only; the app formats labels and decides selectability.
use amm_core::SUPPORTED_FEE_TIERS;
use serde_json::{json, Value};
use super::FeeTiersRequest;
/// The AMM's supported fee tiers as raw basis points, ascending. Pure — no inputs. Wrapped in
/// `{ feeTiers: [...] }` so the op returns an object (the module unwraps it to a bare list).
pub(super) fn fee_tiers(_request: FeeTiersRequest) -> Result<Value, String> {
let tiers: Vec<u64> = SUPPORTED_FEE_TIERS
.iter()
.map(|&bps| u64::try_from(bps).map_err(|_| format!("fee tier {bps} overflows u64")))
.collect::<Result<_, _>>()?;
Ok(json!({ "feeTiers": tiers }))
}
#[cfg(test)]
mod tests {
use amm_core::is_supported_fee_tier;
use super::*;
#[test]
fn fee_tiers_are_amm_core_supported_and_ascending() {
let value = fee_tiers(FeeTiersRequest {}).unwrap();
let tiers: Vec<u64> = value["feeTiers"]
.as_array()
.unwrap()
.iter()
.map(|tier| tier.as_u64().unwrap())
.collect();
assert_eq!(tiers, vec![1, 5, 30, 100]);
for tier in &tiers {
assert!(
is_supported_fee_tier(u128::from(*tier)),
"{tier} bps unsupported"
);
}
}
}
+8 -2
View File
@@ -2,6 +2,7 @@
mod config;
mod context;
mod fee;
mod holding;
mod liquidity;
mod pair;
@@ -18,8 +19,8 @@ use std::{error::Error, fmt};
pub use request::{
AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigIdRequest, ContextRequest,
CreatePoolPlanRequest, CreatePoolQuoteRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest,
RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest,
CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest,
ProgramIdRequest, RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest,
SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
SwapExactOutQuoteRequest, SwapPairRequest, SyncReservesPlanRequest, TokenHoldingsRequest,
TokenIdsRequest,
@@ -156,3 +157,8 @@ pub fn token_holdings(request: TokenHoldingsRequest) -> AmmResult {
pub fn program_id(request: ProgramIdRequest) -> AmmResult {
swap::program_id(request).map_err(Into::into)
}
/// Lists the AMM's supported fee tiers (raw bps) from `amm_core` — no inputs.
pub fn fee_tiers(request: FeeTiersRequest) -> AmmResult {
fee::fee_tiers(request).map_err(Into::into)
}
+6
View File
@@ -287,3 +287,9 @@ pub struct TokenHoldingsRequest {
pub struct ProgramIdRequest {
pub elf: String,
}
/// No inputs — `fee_tiers` enumerates `amm_core::SUPPORTED_FEE_TIERS`. An empty struct so the
/// op keeps the uniform `call::<T>` request-decoding path (the module sends `{}`).
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FeeTiersRequest {}
+10 -5
View File
@@ -7,11 +7,11 @@ use serde::{de::DeserializeOwned, Serialize};
use crate::api::{
self, AddLiquidityPlanRequest, AddLiquidityQuoteRequest, AmmApiError, AmmResult,
ConfigIdRequest, ContextRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest, PairIdsRequest,
PoolIdRequest, ProgramIdRequest, RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest,
ResolvePoolRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
SwapExactOutQuoteRequest, SwapPairRequest, SyncReservesPlanRequest, TokenHoldingsRequest,
TokenIdsRequest,
ConfigIdRequest, ContextRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest,
FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest, RemoveLiquidityPlanRequest,
RemoveLiquidityQuoteRequest, ResolvePoolRequest, SwapExactInPlanRequest,
SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest,
SyncReservesPlanRequest, TokenHoldingsRequest, TokenIdsRequest,
};
#[derive(Serialize)]
@@ -180,6 +180,11 @@ pub extern "C" fn amm_program_id(request_json: *const c_char) -> *mut c_char {
call::<ProgramIdRequest>(request_json, api::program_id)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_fee_tiers(request_json: *const c_char) -> *mut c_char {
call::<FeeTiersRequest>(request_json, api::fee_tiers)
}
/// Releases a string returned by an `amm_*` operation.
///
/// # Safety
+6 -6
View File
@@ -6,11 +6,11 @@ mod ffi;
pub mod api;
pub use api::{
config_id, context, create_pool_plan, create_pool_quote, pair_ids, pool_id, program_id,
resolve_pool, swap_exact_in_plan, swap_exact_in_quote, swap_exact_out_plan,
config_id, context, create_pool_plan, create_pool_quote, fee_tiers, pair_ids, pool_id,
program_id, resolve_pool, swap_exact_in_plan, swap_exact_in_quote, swap_exact_out_plan,
swap_exact_out_quote, swap_pair, token_ids, AccountRead, AmmApiError, AmmResponse, AmmResult,
ConfigIdRequest, ContextRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest, PairIdsRequest,
PoolIdRequest, ProgramIdRequest, ResolvePoolRequest, SwapExactInPlanRequest,
SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest,
TokenIdsRequest, WalletAccount,
ConfigIdRequest, ContextRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest,
FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest, ResolvePoolRequest,
SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
SwapExactOutQuoteRequest, SwapPairRequest, TokenIdsRequest, WalletAccount,
};
+14
View File
@@ -1314,6 +1314,20 @@ LogosList AmmModuleImpl::tokenHoldings(bool wallet_open) {
return out;
}
LogosList AmmModuleImpl::feeTiers() {
// Pure enumeration of amm_core::SUPPORTED_FEE_TIERS — no program id, config,
// or wallet read needed. The FFI wraps the list as { feeTiers: [...] }.
const FfiResult result = call(amm_fee_tiers, json::object());
if (!result.ok)
return LogosList::array();
LogosList out = LogosList::array();
const auto it = result.value.find("feeTiers");
if (it != result.value.end() && it->is_array())
for (const auto& tier : *it) out.push_back(tier);
return out;
}
LogosMap AmmModuleImpl::newPositionContext(const LogosMap& request,
bool wallet_open,
bool refresh_wallet_accounts) {
+7
View File
@@ -204,6 +204,13 @@ public:
/// token_holdings.rs.)
LogosList tokenHoldings(bool wallet_open);
/// Lists the AMM's supported fee tiers as raw basis points, ascending:
/// `[1, 5, 30, 100]`. Pure and input-free — the list is `amm_core`'s
/// `SUPPORTED_FEE_TIERS` (the same set the guest enforces), so the UI's fee
/// selector never hardcodes or drifts from the program. The app formats the
/// labels and decides selectability.
LogosList feeTiers();
/// Reads the token list config at TOKENS_CONFIG (a JSON array of
/// { symbol, name, definitionId, holding, decimals }) and returns it,
/// normalizing definitionId/holding to lowercase hex. Empty list if