chore(amm-ui): swap form activates exact input or output based on the fields updated

closes #56
This commit is contained in:
Andrea Franz 2026-05-08 15:14:23 +02:00
parent 3df3c3d7c4
commit 476087a36b
5 changed files with 97 additions and 27 deletions

View File

@ -11,7 +11,9 @@ Rectangle {
property var tokens: []
property var sellToken: null
property var buyToken: null
property string sellAmount: ""
property string sellInput: ""
property string buyInput: ""
property string editingSide: "sell"
property real slippageTolerancePercent: 0.5
DummySwapState {
@ -28,27 +30,44 @@ Rectangle {
}
function resetAmounts() {
root.sellAmount = ""
root.sellInput = ""
root.buyInput = ""
root.editingSide = "sell"
}
readonly property real sellReserve: sellToken ? (sellToken.reserve || 0) : 0
readonly property real buyReserve: buyToken ? (buyToken.reserve || 0) : 0
readonly property real parsedSellAmount: {
var amt = parseFloat(sellAmount)
readonly property real parsedSellInput: {
var amt = parseFloat(sellInput)
return isNaN(amt) || amt < 0 ? 0 : amt
}
readonly property real parsedBuyAmount: swapState.amountOutFor(parsedSellAmount, sellReserve, buyReserve)
readonly property real parsedBuyInput: {
var amt = parseFloat(buyInput)
return isNaN(amt) || amt < 0 ? 0 : amt
}
readonly property real parsedSellAmount: editingSide === "sell"
? parsedSellInput
: swapState.amountInFor(parsedBuyInput, sellReserve, buyReserve)
readonly property real parsedBuyAmount: editingSide === "buy"
? parsedBuyInput
: swapState.amountOutFor(parsedSellInput, sellReserve, buyReserve)
readonly property real feeAmount: swapState.feeAmount(parsedSellAmount)
readonly property real minReceivedAmount: swapState.minReceived(parsedBuyAmount, slippageTolerancePercent)
readonly property real priceImpactPercent: swapState.priceImpactPercent(parsedSellAmount, parsedBuyAmount, sellReserve, buyReserve)
readonly property bool hasAmount: parsedSellAmount > 0
readonly property string swapMode: editingSide === "buy" ? "swap-exact-output" : "swap-exact-input"
readonly property string swapModeText: editingSide === "buy" ? qsTr("Exact output") : qsTr("Exact input")
readonly property bool hasAmount: editingSide === "sell" ? parsedSellInput > 0 : parsedBuyInput > 0
readonly property bool tokensSelected: sellToken !== null && buyToken !== null
readonly property bool insufficientBalance: hasAmount && sellToken !== null && parsedSellAmount > (sellToken.balance || 0)
readonly property bool insufficientLiquidity: hasAmount && buyToken !== null && parsedBuyAmount > (buyToken.reserve || 0)
readonly property bool canSubmit: tokensSelected && hasAmount && !insufficientBalance && !insufficientLiquidity
readonly property bool canSubmit: tokensSelected && hasAmount && parsedSellAmount > 0 && parsedBuyAmount > 0 && !insufficientBalance && !insufficientLiquidity
readonly property string submitButtonText: {
if (!hasAmount || !tokensSelected) return qsTr("Enter an amount")
@ -63,21 +82,22 @@ Rectangle {
return val.toFixed(8)
}
readonly property string buyAmount: {
if (!sellToken || !buyToken || sellAmount === "") return ""
if (parsedSellAmount <= 0) return ""
return formatAmountValue(parsedBuyAmount)
}
readonly property string sellDisplay: editingSide === "sell"
? sellInput
: (parsedSellAmount > 0 ? formatAmountValue(parsedSellAmount) : "")
readonly property string buyDisplay: editingSide === "buy"
? buyInput
: (parsedBuyAmount > 0 ? formatAmountValue(parsedBuyAmount) : "")
readonly property string sellUsd: {
if (!sellToken || sellAmount === "") return ""
if (parsedSellAmount <= 0) return ""
if (!sellToken || parsedSellAmount <= 0) return ""
var val = parsedSellAmount * sellToken.usdPrice
return "~$" + val.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",")
}
readonly property string buyUsd: {
if (!buyToken || buyAmount === "") return ""
if (!buyToken || parsedBuyAmount <= 0) return ""
var val = parsedBuyAmount * buyToken.usdPrice
return "~$" + val.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",")
}
@ -92,7 +112,9 @@ Rectangle {
"feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""),
"priceImpactPercent": swapState.formatPercent(priceImpactPercent),
"priceImpactPercentValue": priceImpactPercent,
"slippageTolerance": swapState.formatSlippagePercent(slippageTolerancePercent)
"slippageTolerance": swapState.formatSlippagePercent(slippageTolerancePercent),
"swapMode": swapMode,
"swapModeText": swapModeText
}
}
@ -117,11 +139,14 @@ Rectangle {
Layout.fillWidth: true
theme: root.theme
label: "Sell"
amount: root.sellAmount
amount: root.sellDisplay
usdValue: root.sellUsd
token: root.sellToken
readOnly: false
onInputEdited: function(v) { root.sellAmount = v }
active: root.editingSide === "sell"
onInputEdited: function(v) {
root.sellInput = v
if (root.editingSide !== "sell") root.editingSide = "sell"
}
onTokenClicked: root.requestTokenSelect("sell")
}
@ -170,10 +195,14 @@ Rectangle {
Layout.fillWidth: true
theme: root.theme
label: "Buy"
amount: root.buyAmount
amount: root.buyDisplay
usdValue: root.buyUsd
token: root.buyToken
readOnly: true
active: root.editingSide === "buy"
onInputEdited: function(v) {
root.buyInput = v
if (root.editingSide !== "buy") root.editingSide = "buy"
}
onTokenClicked: root.requestTokenSelect("buy")
}
@ -184,6 +213,7 @@ Rectangle {
Layout.rightMargin: 16
theme: root.theme
visible: root.tokensSelected && root.hasAmount
swapModeText: root.swapModeText
feeText: swapState.formatTokenAmount(root.feeAmount, root.sellToken ? root.sellToken.symbol : "")
priceImpactText: swapState.formatPercent(root.priceImpactPercent)
priceImpactPercent: root.priceImpactPercent

View File

@ -9,7 +9,7 @@ Rectangle {
property string amount: ""
property string usdValue: ""
property var token: null
property bool readOnly: false
property bool active: true
signal tokenClicked()
signal inputEdited(string newValue)
@ -18,11 +18,10 @@ Rectangle {
target: tiInput
property: "text"
value: root.amount
when: root.readOnly
}
radius: 16
color: theme.colors.inputBg
color: root.active ? theme.colors.inputBg : theme.colors.panelBg
implicitHeight: 110
Behavior on color { ColorAnimation { duration: 300 } }
@ -52,13 +51,12 @@ Rectangle {
TextInput {
id: tiInput
anchors.fill: parent
color: theme.colors.textPrimary
color: root.active ? theme.colors.textPrimary : theme.colors.textSecondary
font.pixelSize: 36
font.weight: Font.Bold
readOnly: root.readOnly
selectionColor: theme.colors.selection
clip: true
onTextChanged: { if (!root.readOnly) root.inputEdited(text) }
onTextEdited: root.inputEdited(text)
validator: RegularExpressionValidator {
regularExpression: /^[0-9]*\.?[0-9]*$/
}

View File

@ -144,6 +144,7 @@ FocusScope {
SwapSummary {
Layout.fillWidth: true
theme: root.theme
swapModeText: root.snapshot.swapModeText || ""
feeText: root.snapshot.feeAmount || ""
priceImpactText: root.snapshot.priceImpactPercent || ""
priceImpactPercent: Number(root.snapshot.priceImpactPercentValue) || 0

View File

@ -5,6 +5,7 @@ Item {
id: root
property var theme
property string swapModeText: ""
property string feeText: ""
property string priceImpactText: ""
property real priceImpactPercent: 0
@ -25,6 +26,30 @@ Item {
anchors.fill: parent
spacing: 8
Item {
implicitHeight: 18
visible: root.swapModeText.length > 0
Layout.fillWidth: true
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: root.theme.colors.textSecondary
font.pixelSize: 12
text: qsTr("Type of swap")
}
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: root.theme.colors.textPrimary
font.bold: true
font.pixelSize: 12
text: root.swapModeText
}
}
Item {
implicitHeight: 18

View File

@ -30,6 +30,22 @@ QtObject {
return safeReserveOut * amountInAfterFee / (safeReserveIn + amountInAfterFee);
}
function amountInFor(amountOut, reserveIn, reserveOut) {
const safeAmountOut = parseAmount(amountOut);
const safeReserveIn = parseAmount(reserveIn);
const safeReserveOut = parseAmount(reserveOut);
if (safeAmountOut <= 0 || safeReserveIn <= 0 || safeReserveOut <= 0) {
return 0;
}
if (safeAmountOut >= safeReserveOut) {
return 0;
}
const amountInAfterFee = safeAmountOut * safeReserveIn / (safeReserveOut - safeAmountOut);
return amountInAfterFee * 10000 / (10000 - root.feeBps);
}
function priceImpactPercent(amountIn, amountOut, reserveIn, reserveOut) {
const safeAmountIn = parseAmount(amountIn);
const safeAmountOut = parseAmount(amountOut);