mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
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:
@@ -67,8 +67,13 @@ AmmActionCard {
|
||||
})
|
||||
readonly property var tokens: root.newPositionContext && root.newPositionContext.tokens
|
||||
? root.newPositionContext.tokens : []
|
||||
readonly property var feeTiers: root.newPositionContext && root.newPositionContext.feeTiers
|
||||
? root.newPositionContext.feeTiers : []
|
||||
// Supported fee tiers as raw bps, injected from backend.feeTiers() (amm_core's
|
||||
// SUPPORTED_FEE_TIERS). The selector's delegate wants { feeBps } rows, so wrap
|
||||
// each int; labels are derived locally via feeLabel().
|
||||
property var feeTiers: []
|
||||
readonly property var feeTierModel: (root.feeTiers || []).map(function(bps) {
|
||||
return { "feeBps": Number(bps) }
|
||||
})
|
||||
readonly property var tokenA: root.tokenById(root.selectedTokenAId)
|
||||
readonly property var tokenB: root.tokenById(root.selectedTokenBId)
|
||||
readonly property int decimalsA: 0
|
||||
@@ -407,7 +412,7 @@ AmmActionCard {
|
||||
rowSpacing: 8
|
||||
|
||||
Repeater {
|
||||
model: root.feeTiers
|
||||
model: root.feeTierModel
|
||||
|
||||
Item {
|
||||
id: feeTierOption
|
||||
|
||||
@@ -21,6 +21,11 @@ Item {
|
||||
// account selectors; refetched when the wallet opens.
|
||||
property var holdings: []
|
||||
|
||||
// The AMM's supported fee tiers (backend.feeTiers()) feeding the fee selector.
|
||||
// Program-derived and wallet-independent, so it's fetched once when the backend
|
||||
// becomes available.
|
||||
property var feeTiers: []
|
||||
|
||||
function refreshHoldings() {
|
||||
if (!root.backend || root.runtime === null)
|
||||
return
|
||||
@@ -29,9 +34,17 @@ Item {
|
||||
function(err) { console.warn("tokenHoldings error:", err) })
|
||||
}
|
||||
|
||||
onBackendChanged: root.refreshHoldings()
|
||||
onRuntimeChanged: root.refreshHoldings()
|
||||
Component.onCompleted: root.refreshHoldings()
|
||||
function refreshFeeTiers() {
|
||||
if (!root.backend || root.runtime === null || root.feeTiers.length > 0)
|
||||
return
|
||||
root.runtime.watch(root.backend.feeTiers(),
|
||||
function(list) { root.feeTiers = list },
|
||||
function(err) { console.warn("feeTiers error:", err) })
|
||||
}
|
||||
|
||||
onBackendChanged: { root.refreshHoldings(); root.refreshFeeTiers() }
|
||||
onRuntimeChanged: { root.refreshHoldings(); root.refreshFeeTiers() }
|
||||
Component.onCompleted: { root.refreshHoldings(); root.refreshFeeTiers() }
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
@@ -237,6 +250,7 @@ onBackendChanged: root.refreshHoldings()
|
||||
: qsTr("Choose two tokens and a fee tier for this position.")
|
||||
showRefreshAction: false
|
||||
holdings: root.holdings
|
||||
feeTiers: root.feeTiers
|
||||
newPositionContext: newPositionFlow.newPositionContext
|
||||
flowState: newPositionFlow.viewState
|
||||
|
||||
|
||||
@@ -466,8 +466,7 @@ QtObject {
|
||||
function loadingContext() {
|
||||
return {
|
||||
"status": "loading",
|
||||
"tokens": [],
|
||||
"feeTiers": []
|
||||
"tokens": []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -315,6 +315,14 @@ QVariantList AmmUiBackend::poolList()
|
||||
return readPoolsConfig();
|
||||
}
|
||||
|
||||
QVariantList AmmUiBackend::feeTiers()
|
||||
{
|
||||
// Pure, input-free enumeration of the AMM's supported fee tiers (raw bps) —
|
||||
// no wallet or module connection state involved.
|
||||
return m_logos->amm_module.feeTiers();
|
||||
}
|
||||
|
||||
|
||||
QVariantMap AmmUiBackend::createPool(QVariantMap request)
|
||||
{
|
||||
// Same connected-state submit guard as the swaps — this app's lock is
|
||||
|
||||
@@ -85,6 +85,8 @@ public slots:
|
||||
// Reads the known-pools list from AMM_POOLS_CONFIG (app config JSON, read
|
||||
// here rather than in the amm_module — pool discovery is an app detail).
|
||||
QVariantList poolList() override;
|
||||
// The AMM's supported fee tiers (raw bps) for the fee selector.
|
||||
QVariantList feeTiers() override;
|
||||
|
||||
private:
|
||||
void syncWalletState();
|
||||
|
||||
@@ -144,4 +144,10 @@ class AmmUiBackend
|
||||
// list if AMM_POOLS_CONFIG is unset/unreadable/invalid. This is app config, not
|
||||
// an on-chain read, so it lives in the backend rather than the amm_module.
|
||||
SLOT(QVariantList poolList())
|
||||
|
||||
// The AMM's supported fee tiers as raw basis points, ascending: [1, 5, 30,
|
||||
// 100]. Input-free — sourced from amm_core's SUPPORTED_FEE_TIERS (the set the
|
||||
// guest enforces), so the fee selector never hardcodes or drifts. The QML
|
||||
// formats labels and decides selectability.
|
||||
SLOT(QVariantList feeTiers())
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -257,6 +257,17 @@ pub const FEE_TIER_BPS_5: u128 = 5;
|
||||
pub const FEE_TIER_BPS_30: u128 = 30;
|
||||
pub const FEE_TIER_BPS_100: u128 = 100;
|
||||
|
||||
/// The supported fee tiers (raw bps), ascending — the canonical list for off-chain callers
|
||||
/// (the module/UI) to enumerate without hardcoding. The guest's check stays `is_supported_fee_tier`
|
||||
/// below (its `match` is unchanged, so this addition does not affect the program ImageID — the
|
||||
/// const is unused on-chain); `fee_tiers_list_matches_the_check` locks the two together.
|
||||
pub const SUPPORTED_FEE_TIERS: [u128; 4] = [
|
||||
FEE_TIER_BPS_1,
|
||||
FEE_TIER_BPS_5,
|
||||
FEE_TIER_BPS_30,
|
||||
FEE_TIER_BPS_100,
|
||||
];
|
||||
|
||||
pub fn is_supported_fee_tier(fees: u128) -> bool {
|
||||
matches!(
|
||||
fees,
|
||||
@@ -677,6 +688,26 @@ mod tests {
|
||||
/// `1.0` in Q64.64 is `2^64`.
|
||||
const ONE_Q64_64: u128 = 1u128 << 64;
|
||||
|
||||
#[test]
|
||||
fn fee_tiers_list_matches_the_check() {
|
||||
// The enumerated list must agree with the guest's `is_supported_fee_tier` match: every
|
||||
// listed tier is accepted, and nothing adjacent to them is (guards a drifted list).
|
||||
for tier in SUPPORTED_FEE_TIERS {
|
||||
assert!(
|
||||
is_supported_fee_tier(tier),
|
||||
"{tier} listed but not accepted"
|
||||
);
|
||||
}
|
||||
for probe in [0u128, 2, 4, 6, 29, 31, 99, 101, 10_000] {
|
||||
assert!(
|
||||
!is_supported_fee_tier(probe),
|
||||
"{probe} accepted but not listed"
|
||||
);
|
||||
}
|
||||
// Ascending and de-duplicated.
|
||||
assert!(SUPPORTED_FEE_TIERS.windows(2).all(|w| w[0] < w[1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_reserves_map_to_unit_price() {
|
||||
assert_eq!(spot_price_q64_64(1_000, 1_000), ONE_Q64_64);
|
||||
|
||||
Reference in New Issue
Block a user