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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ Item {
id: root id: root
property var theme property var theme
property string swapModeText: ""
property string feeText: "" property string feeText: ""
property string priceImpactText: "" property string priceImpactText: ""
property real priceImpactPercent: 0 property real priceImpactPercent: 0
@ -25,6 +26,30 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: 8 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 { Item {
implicitHeight: 18 implicitHeight: 18

View File

@ -30,6 +30,22 @@ QtObject {
return safeReserveOut * amountInAfterFee / (safeReserveIn + amountInAfterFee); 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) { function priceImpactPercent(amountIn, amountOut, reserveIn, reserveOut) {
const safeAmountIn = parseAmount(amountIn); const safeAmountIn = parseAmount(amountIn);
const safeAmountOut = parseAmount(amountOut); const safeAmountOut = parseAmount(amountOut);