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
@@ -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
+17 -3
View File
@@ -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
+1 -2
View File
@@ -466,8 +466,7 @@ QtObject {
function loadingContext() {
return {
"status": "loading",
"tokens": [],
"feeTiers": []
"tokens": []
}
}
+8
View File
@@ -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
+2
View File
@@ -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();
+6
View File
@@ -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())
}