Files
lez-programs/apps/amm/qml/components/liquidity/TokenAmountInput.qml
r4bbit eb98aac31f feat(amm): drive the create-pool liquidity preview from liquidityQuote
Both liquidity branches now quote through the lean composable ops. The create
path joins the add path (already on addLiquidityQuote) by reworking
liquidity_quote into a dual-mode create quote and wiring the form to it via
the resolvePool pool read. quoteNewPosition/amm_quote are no longer reached
from the UI.

FFI (modules/amm/ffi):
- liquidity_quote is dual-mode: price-only (initialPriceRealRaw, no amounts)
  returns the minimum opening deposit via minimum_opening_pair; supplied
  amounts return the actual deposit with the price derived from them. Emits
  actual/minimum amounts, expectedLp, lockedLp and the Q64.64 price.
- LiquidityQuoteRequest gains initial_price_real_raw (Option<String>, needed
  only in price-only mode).

Module (modules/amm/src):
- liquidityQuote forwards initialPriceRealRaw to the op.

UI (apps/amm/qml):
- Route create-vs-add on the pool read (resolvePool -> poolExists); create
  quotes via liquidityQuote, assembled into the missing-pool shape the form
  already consumes.
- poolStatus moves off the quote onto the flow's poolExists; the form derives
  activePool/missingPool from it. Trim the vestigial quote fields (canSubmit,
  requiresFreshLp, warnings, errors[], accountPreview, the "Pool" row) and drop
  the account-plan panel for parity with the swap view.
- Fix a real bug: a pair change now resets poolExists (resetPoolExistence) so
  resetPairDraft re-resolves the pool like a fresh selection. Otherwise an
  active pool kept stale (cleared) reserves with no re-quote, and the deposit
  ratio-fill silently no-op'd.

Tests (apps/amm/tests):
- Read activePool instead of the removed poolStatus. The add test waits for the
  reset's active-pool quote to settle (reserves reloaded) before the ratio-fill;
  the create test resets the draft to clear leftover cross-run amounts and the
  stale submitted transactionId.
2026-08-11 17:51:51 +02:00

192 lines
6.6 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import Logos.Wallet
import "../shared"
AmmTokenAmountSurface {
id: root
property string text: ""
property string balance: ""
property string helperText: ""
property bool showMaxButton: true
property var tokenData: null
property var tokens: []
property string selectedTokenId: ""
property bool tokenInvalid: false
property bool tokenSelectionEnabled: true
property bool editPending: false
property string pendingValue: ""
// Account selector (create-pool only): the wallet holdings to pick from, the
// token's base58 definitionId to filter by, and whether to show it at all. The
// chosen holding id is exposed as selectedHoldingId. Mirrors the swap card, where
// the selector sits inside the input card below the token button.
property var holdings: []
property string holdingDefinitionId: ""
property bool showHoldingSelector: false
// objectName forwarded to the account selector, so UI tests can pick the
// funding holding for this side deterministically.
property string selectorObjectName: ""
readonly property string selectedHoldingId: root.footerItem && root.footerItem.selectedAccountId
? String(root.footerItem.selectedAccountId) : ""
// Base-unit balance of the selected funding holding (the lean quotes don't check funding,
// so the form compares this against the entered amount). "0" when nothing is selected.
readonly property string selectedBalanceRaw: root.footerItem && root.footerItem.selectedBalanceRaw
? String(root.footerItem.selectedBalanceRaw) : "0"
footer: root.showHoldingSelector ? accountFooter : null
footerHeight: root.footerItem ? root.footerItem.implicitHeight : 0
property var disabledReasonForCode: function(code) {
return qsTr("This token is unavailable (%1).").arg(code || "unknown")
}
property var detailForToken: function(token) { return "" }
property alias popup: tokenModal
property alias query: tokenModal.searchText
readonly property var rows: tokenModal.rows
signal editingChanged(string value)
signal editingCommitted(string value)
signal maxClicked
signal tokenSelected(string tokenId)
signal tokenEntered(string value)
amount: root.text
supportingText: root.helperText
supportingActionText: root.showMaxButton ? qsTr("MAX") : ""
accessory: tokenActions
accessoryWidth: width < 360 ? 132 : 180
accessoryHeight: root.balance.length > 0 ? 58 : 40
onAmountEdited: function(value) {
root.pendingValue = value
root.editPending = true
root.editingChanged(value)
commitTimer.restart()
}
onAmountEditingFinished: function(value) {
root.pendingValue = value
root.commitPendingEdit()
}
onSupportingActionClicked: root.maxClicked()
onTextChanged: {
if (root.editPending && root.text !== root.pendingValue) {
commitTimer.stop()
root.editPending = false
}
}
Timer {
id: commitTimer
interval: 250
repeat: false
onTriggered: root.commitPendingEdit()
}
Component {
id: accountFooter
// The Loader stretches this wrapper to the card width; the selector takes the
// right half, right-aligned, matching the swap card's account selector.
Item {
implicitHeight: footerSelector.implicitHeight
property alias selectedAccountId: footerSelector.selectedAccountId
property alias selectedBalanceRaw: footerSelector.selectedBalanceRaw
ProgramAccountSelector {
id: footerSelector
objectName: root.selectorObjectName
width: Math.round(parent.width / 2)
anchors.right: parent.right
anchors.top: parent.top
sourceModel: root.holdings
accountType: "TokenHolding"
stateField: "definitionId"
stateValue: root.holdingDefinitionId
selectionMode: ProgramAccountSelector.Input
showWhenSingle: true
textAlignment: Text.AlignRight
backgroundColor: root.theme.colors.panelBg
hoverColor: root.theme.colors.panelHoverBg
textColor: root.theme.colors.textPrimary
secondaryTextColor: root.theme.colors.textSecondary
borderColor: root.theme.colors.borderStrong
focusColor: root.theme.colors.ctaBg
}
}
}
Component {
id: tokenActions
AmmTokenAccessory {
theme: root.theme
enabled: root.tokenSelectionEnabled
invalid: root.tokenInvalid
hasToken: root.tokenData !== null
tokenColor: root.tokenColor(root.tokenData)
tokenLetter: root.tokenLetter(root.tokenData)
tokenText: root.tokenText(root.tokenData)
balance: root.balance
accessibleName: qsTr("Select %1").arg(root.label)
onClicked: tokenModal.open()
}
}
TokenSelectorModal {
id: tokenModal
theme: root.theme
tokens: root.tokens
title: qsTr("Select a token")
searchPlaceholder: qsTr("Search name or address")
popularTitle: qsTr("Quick select")
listTitle: qsTr("All tokens")
allowCustomEntry: true
disabledReasonForCode: root.disabledReasonForCode
detailForToken: root.detailForToken
onTokenSelected: function(token) {
root.tokenSelected(String(token.definitionId || token.address || ""))
}
onTokenEntered: function(value) { root.tokenEntered(value) }
}
function acceptInput(value) {
tokenModal.acceptInput(value)
}
function commitPendingEdit() {
if (!root.editPending)
return
commitTimer.stop()
root.editPending = false
root.editingCommitted(root.pendingValue)
}
function tokenText(token) {
if (!token)
return qsTr("Select token")
return String(token.symbol || token.name || root.shortId(root.selectedTokenId))
}
function tokenLetter(token) {
var text = root.tokenText(token)
return token ? String(token.letter || text.charAt(0).toUpperCase()) : ""
}
function tokenColor(token) {
return token && token.color ? token.color : root.theme.colors.noTokenCircle
}
function shortId(value) {
var text = String(value || "")
return text.length > 14 ? text.slice(0, 7) + "..." + text.slice(-5) : text
}
}