518 lines
24 KiB
QML
518 lines
24 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Layouts 1.15
|
|
import QtQml.Models 2.15
|
|
|
|
import utils 1.0
|
|
|
|
import StatusQ.Controls 0.1
|
|
import StatusQ.Core 0.1
|
|
import StatusQ.Core.Backpressure 0.1
|
|
import StatusQ.Core.Theme 0.1
|
|
import StatusQ.Core.Utils 0.1 as SQUtils
|
|
import StatusQ.Popups.Dialog 0.1
|
|
|
|
import shared.popups.send.controls 1.0
|
|
import shared.controls 1.0
|
|
|
|
import AppLayouts.Wallet.controls 1.0
|
|
import AppLayouts.Wallet.panels 1.0
|
|
|
|
StatusDialog {
|
|
id: root
|
|
|
|
/* This should be the only property which should be used to input
|
|
parameters to the modal when being launched from elsewhere */
|
|
required property SwapInputParamsForm swapInputParamsForm
|
|
required property SwapModalAdaptor swapAdaptor
|
|
required property int loginType
|
|
|
|
objectName: "swapModal"
|
|
|
|
implicitWidth: 556
|
|
padding: 0
|
|
backgroundColor: Theme.palette.baseColor3
|
|
|
|
QtObject {
|
|
id: d
|
|
property var debounceFetchSuggestedRoutes: Backpressure.debounce(root, 1000, function() {
|
|
root.swapAdaptor.fetchSuggestedRoutes(payPanel.rawValue)
|
|
})
|
|
|
|
property Timer autoRefreshTimer: Timer {
|
|
interval: root.swapInputParamsForm.autoRefreshTime
|
|
running: false
|
|
repeat: false
|
|
onTriggered: d.fetchSuggestedRoutes()
|
|
}
|
|
|
|
function fetchSuggestedRoutes() {
|
|
if (root.swapInputParamsForm.isFormFilledCorrectly()) {
|
|
root.swapAdaptor.validSwapProposalReceived = false
|
|
root.swapAdaptor.swapProposalLoading = true
|
|
root.swapAdaptor.approvalPending = false
|
|
root.swapAdaptor.approvalSuccessful = false
|
|
root.swapAdaptor.swapOutputData.resetPathInfoAndError()
|
|
debounceFetchSuggestedRoutes()
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root.swapInputParamsForm
|
|
function onFormValuesChanged() {
|
|
d.fetchSuggestedRoutes()
|
|
}
|
|
// refresh the selected asset in payPanel when account/network changes
|
|
function onSelectedAccountAddressChanged() {
|
|
payPanel.reevaluateSelectedId()
|
|
}
|
|
function onSelectedNetworkChainIdChanged() {
|
|
networkFilter.selection = [root.swapInputParamsForm.selectedNetworkChainId]
|
|
payPanel.reevaluateSelectedId()
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root.swapAdaptor
|
|
function onApprovalSuccessfulChanged() {
|
|
// perform a recalculation to make sure expected outcome shown is accurate
|
|
if(root.swapAdaptor.approvalSuccessful) {
|
|
d.fetchSuggestedRoutes()
|
|
}
|
|
}
|
|
function onSuggestedRoutesReady() {
|
|
if(!root.swapAdaptor.swapProposalLoading)
|
|
d.autoRefreshTimer.restart()
|
|
}
|
|
}
|
|
|
|
Behavior on implicitHeight {
|
|
NumberAnimation { duration: 1000; easing.type: Easing.OutExpo; alwaysRunToEnd: true}
|
|
}
|
|
|
|
onOpened: payPanel.forceActiveFocus()
|
|
onClosed: root.swapAdaptor.reset()
|
|
|
|
header: Item {
|
|
implicitHeight: selector.implicitHeight
|
|
implicitWidth: selector.implicitWidth
|
|
anchors.top: parent.top
|
|
anchors.topMargin: -height - 18
|
|
AccountSelectorHeader {
|
|
id: selector
|
|
control.popup.width: 512
|
|
model: root.swapAdaptor.nonWatchAccounts
|
|
selectedAddress: root.swapInputParamsForm.selectedAccountAddress
|
|
onCurrentAccountAddressChanged: {
|
|
if (currentAccountAddress !== "" && currentAccountAddress !== root.swapInputParamsForm.selectedAccountAddress) {
|
|
root.swapInputParamsForm.selectedAccountAddress = currentAccountAddress
|
|
}
|
|
}
|
|
control.popup.onClosed: payPanel.forceActiveFocus()
|
|
}
|
|
}
|
|
|
|
StatusScrollView {
|
|
anchors.fill: parent
|
|
contentWidth: availableWidth
|
|
topPadding: 0
|
|
bottomPadding: Style.current.xlPadding
|
|
|
|
ColumnLayout {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Style.current.halfPadding
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Style.current.halfPadding
|
|
spacing: Style.current.padding
|
|
clip: true
|
|
|
|
// without this Column, the whole popup resizing when the network selector popup is clicked
|
|
Column {
|
|
Layout.fillWidth: true
|
|
spacing: 0
|
|
RowLayout {
|
|
width: parent.width
|
|
spacing: 12
|
|
HeaderTitleText {
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
|
text: qsTr("Swap")
|
|
}
|
|
StatusBaseText {
|
|
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
|
text: qsTr("On:")
|
|
color: Theme.palette.baseColor1
|
|
font.pixelSize: 13
|
|
lineHeight: 38
|
|
lineHeightMode: Text.FixedHeight
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
NetworkFilter {
|
|
id: networkFilter
|
|
objectName: "networkFilter"
|
|
Layout.alignment: Qt.AlignVCenter
|
|
multiSelection: false
|
|
showSelectionIndicator: false
|
|
showTitle: false
|
|
flatNetworks: root.swapAdaptor.filteredFlatNetworksModel
|
|
selection: [root.swapInputParamsForm.selectedNetworkChainId]
|
|
onSelectionChanged: {
|
|
if (root.swapInputParamsForm.selectedNetworkChainId !== selection[0]) {
|
|
root.swapInputParamsForm.selectedNetworkChainId = selection[0]
|
|
if(!!root.swapAdaptor.fromToken && !!root.swapAdaptor.fromToken.addressPerChain) {
|
|
let fromTokenAddressOnSelectedChain = SQUtils.ModelUtils.getByKey(
|
|
root.swapAdaptor.fromToken.addressPerChain, "chainId",
|
|
root.swapInputParamsForm.selectedNetworkChainId, "address")
|
|
if(!fromTokenAddressOnSelectedChain) {
|
|
// reset from token as it doesnt exist on selected network
|
|
root.swapInputParamsForm.resetFromTokenValues()
|
|
}
|
|
}
|
|
if(!!root.swapAdaptor.toToken && !!root.swapAdaptor.toToken.addressPerChain) {
|
|
let toTokenAddressOnSelectedChain = SQUtils.ModelUtils.getByKey(
|
|
root.swapAdaptor.toToken.addressPerChain, "chainId",
|
|
root.swapInputParamsForm.selectedNetworkChainId, "address")
|
|
if(!toTokenAddressOnSelectedChain) {
|
|
// reset to token as it doesnt exist on selected network
|
|
root.swapInputParamsForm.resetToTokenValues(false)
|
|
}
|
|
}
|
|
}
|
|
payPanel.forceActiveFocus()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 2
|
|
Layout.preferredHeight: payPanel.height + receivePanel.height + 4
|
|
|
|
SwapInputPanel {
|
|
id: payPanel
|
|
objectName: "payPanel"
|
|
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
top: parent.top
|
|
}
|
|
|
|
currencyStore: root.swapAdaptor.currencyStore
|
|
flatNetworksModel: root.swapAdaptor.swapStore.flatNetworks
|
|
processedAssetsModel: root.swapAdaptor.walletAssetsStore.groupedAccountAssetsModel
|
|
plainTokensBySymbolModel: root.swapAdaptor.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel
|
|
|
|
tokenKey: root.swapInputParamsForm.fromTokensKey
|
|
tokenAmount: root.swapInputParamsForm.fromTokenAmount
|
|
|
|
selectedNetworkChainId: root.swapInputParamsForm.selectedNetworkChainId
|
|
selectedAccountAddress: root.swapInputParamsForm.selectedAccountAddress
|
|
nonInteractiveTokensKey: receivePanel.selectedHoldingId
|
|
|
|
swapSide: SwapInputPanel.SwapSide.Pay
|
|
swapExchangeButtonWidth: swapExchangeButton.width
|
|
|
|
onSelectedHoldingIdChanged: {
|
|
if(!!selectedHoldingId) {
|
|
root.swapInputParamsForm.fromTokensKey = selectedHoldingId
|
|
}
|
|
}
|
|
onRawValueChanged: {
|
|
if(root.swapInputParamsForm.fromTokensKey === selectedHoldingId) {
|
|
const amount = !tokenAmount && value === 0 ? "" :
|
|
SQUtils.AmountsArithmetic.div(SQUtils.AmountsArithmetic.fromString(rawValue),
|
|
SQUtils.AmountsArithmetic.fromNumber(1, rawValueMultiplierIndex)).toString()
|
|
root.swapInputParamsForm.fromTokenAmount = amount
|
|
}
|
|
}
|
|
}
|
|
|
|
SwapInputPanel {
|
|
id: receivePanel
|
|
objectName: "receivePanel"
|
|
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
bottom: parent.bottom
|
|
}
|
|
|
|
currencyStore: root.swapAdaptor.currencyStore
|
|
flatNetworksModel: root.swapAdaptor.swapStore.flatNetworks
|
|
processedAssetsModel: root.swapAdaptor.walletAssetsStore.groupedAccountAssetsModel
|
|
plainTokensBySymbolModel: root.swapAdaptor.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel
|
|
|
|
tokenKey: root.swapInputParamsForm.toTokenKey
|
|
tokenAmount: root.swapAdaptor.validSwapProposalReceived && root.swapAdaptor.toToken ? root.swapAdaptor.swapOutputData.toTokenAmount: root.swapInputParamsForm.toTokenAmount
|
|
|
|
selectedNetworkChainId: root.swapInputParamsForm.selectedNetworkChainId
|
|
selectedAccountAddress: root.swapInputParamsForm.selectedAccountAddress
|
|
nonInteractiveTokensKey: payPanel.selectedHoldingId
|
|
|
|
swapSide: SwapInputPanel.SwapSide.Receive
|
|
swapExchangeButtonWidth: swapExchangeButton.width
|
|
|
|
mainInputLoading: root.swapAdaptor.swapProposalLoading
|
|
bottomTextLoading: root.swapAdaptor.swapProposalLoading
|
|
|
|
onSelectedHoldingIdChanged: {
|
|
if(!!selectedHoldingId) {
|
|
root.swapInputParamsForm.toTokenKey = selectedHoldingId
|
|
}
|
|
}
|
|
|
|
/* TODO: keep this input as disabled until the work for adding a param to handle to
|
|
and from tokens inputed is supported by backend under
|
|
https://github.com/status-im/status-desktop/issues/15095 */
|
|
interactive: false
|
|
}
|
|
|
|
SwapExchangeButton {
|
|
id: swapExchangeButton
|
|
objectName: "swapExchangeButton"
|
|
anchors.centerIn: parent
|
|
onClicked: {
|
|
const tempPayToken = root.swapInputParamsForm.fromTokensKey
|
|
const tempPayAmount = root.swapInputParamsForm.fromTokenAmount
|
|
root.swapInputParamsForm.fromTokensKey = root.swapInputParamsForm.toTokenKey
|
|
root.swapInputParamsForm.fromTokenAmount = !!root.swapAdaptor.swapOutputData.toTokenAmount ? root.swapAdaptor.swapOutputData.toTokenAmount : root.swapInputParamsForm.toTokenAmount
|
|
root.swapInputParamsForm.toTokenKey = tempPayToken
|
|
root.swapInputParamsForm.toTokenAmount = tempPayAmount
|
|
payPanel.forceActiveFocus()
|
|
}
|
|
}
|
|
}
|
|
|
|
EditSlippagePanel {
|
|
id: editSlippagePanel
|
|
objectName: "editSlippagePanel"
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Style.current.padding
|
|
visible: editSlippageButton.checked
|
|
selectedToToken: root.swapAdaptor.toToken
|
|
toTokenAmount: root.swapAdaptor.swapOutputData.toTokenAmount
|
|
loading: root.swapAdaptor.swapProposalLoading
|
|
onSlippageValueChanged: {
|
|
root.swapInputParamsForm.selectedSlippage = slippageValue
|
|
}
|
|
}
|
|
|
|
ErrorTag {
|
|
objectName: "errorTag"
|
|
visible: root.swapAdaptor.swapOutputData.hasError || payPanel.amountEnteredGreaterThanBalance
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.topMargin: Style.current.smallPadding
|
|
text: {
|
|
if (root.swapAdaptor.swapOutputData.hasError) {
|
|
return qsTr("An error has occured, please try again")
|
|
} else if (payPanel.amountEnteredGreaterThanBalance) {
|
|
return qsTr("Insufficient funds for swap")
|
|
}
|
|
}
|
|
buttonText: qsTr("Buy crypto")
|
|
buttonVisible: !root.swapAdaptor.swapOutputData.hasError && payPanel.amountEnteredGreaterThanBalance
|
|
onButtonClicked: Global.openBuyCryptoModalRequested()
|
|
}
|
|
}
|
|
}
|
|
|
|
footer: StatusDialogFooter {
|
|
color: Theme.palette.baseColor3
|
|
dropShadowEnabled: true
|
|
leftButtons: ObjectModel {
|
|
ColumnLayout {
|
|
Layout.leftMargin: Style.current.padding
|
|
spacing: 0
|
|
StatusBaseText {
|
|
objectName: "maxSlippageText"
|
|
text: qsTr("Max slippage:")
|
|
color: Theme.palette.directColor5
|
|
font.weight: Font.Medium
|
|
}
|
|
RowLayout {
|
|
StatusBaseText {
|
|
objectName: "maxSlippageValue"
|
|
text: "%1%".arg(LocaleUtils.numberToLocaleString(root.swapInputParamsForm.selectedSlippage))
|
|
color: Theme.palette.directColor4
|
|
font.weight: Font.Medium
|
|
}
|
|
StatusFlatButton {
|
|
id: editSlippageButton
|
|
objectName: "editSlippageButton"
|
|
checkable: true
|
|
checked: false
|
|
icon.name: "edit_pencil"
|
|
textColor: editSlippageButton.hovered ? Theme.palette.directColor1 : Theme.palette.directColor5
|
|
size: StatusBaseButton.Size.Tiny
|
|
hoverColor: Theme.palette.transparent
|
|
visible: !checked
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rightButtons: ObjectModel {
|
|
RowLayout {
|
|
Layout.rightMargin: Style.current.padding
|
|
spacing: Style.current.bigPadding
|
|
ColumnLayout {
|
|
StatusBaseText {
|
|
objectName: "maxFeesText"
|
|
text: qsTr("Max fees:")
|
|
color: Theme.palette.directColor5
|
|
font.weight: Font.Medium
|
|
}
|
|
StatusTextWithLoadingState {
|
|
objectName: "maxFeesValue"
|
|
text: {
|
|
if(root.swapAdaptor.swapProposalLoading) {
|
|
return Constants.dummyText
|
|
}
|
|
|
|
if(root.swapAdaptor.validSwapProposalReceived) {
|
|
return root.swapAdaptor.currencyStore.formatCurrencyAmount(
|
|
root.swapAdaptor.swapOutputData.totalFees,
|
|
root.swapAdaptor.currencyStore.currentCurrency)
|
|
}
|
|
|
|
return "--"
|
|
}
|
|
customColor: Theme.palette.directColor4
|
|
font.weight: Font.Medium
|
|
loading: root.swapAdaptor.swapProposalLoading
|
|
}
|
|
}
|
|
/* TODO: https://github.com/status-im/status-desktop/issues/15313
|
|
will introduce having loading button and showing text on the side*/
|
|
StatusButton {
|
|
objectName: "signButton"
|
|
readonly property string fromTokenSymbol: !!root.swapAdaptor.fromToken ? root.swapAdaptor.fromToken.symbol ?? "" : ""
|
|
loading: root.swapAdaptor.approvalPending
|
|
icon.name: root.swapAdaptor.selectedAccount.migratedToKeycard ? Constants.authenticationIconByType[Constants.LoginType.Keycard]
|
|
: Constants.authenticationIconByType[root.loginType]
|
|
text: {
|
|
if(root.swapAdaptor.validSwapProposalReceived) {
|
|
if (root.swapAdaptor.approvalPending) {
|
|
return qsTr("Approving %1").arg(fromTokenSymbol)
|
|
} else if(root.swapAdaptor.swapOutputData.approvalNeeded) {
|
|
return qsTr("Approve %1").arg(fromTokenSymbol)
|
|
}
|
|
}
|
|
return qsTr("Swap")
|
|
}
|
|
tooltip.text: root.swapAdaptor.validSwapProposalReceived &&
|
|
root.swapAdaptor.swapOutputData.approvalNeeded ?
|
|
qsTr("Approve %1 spending cap to Swap").arg(fromTokenSymbol) : ""
|
|
disabledColor: Theme.palette.directColor8
|
|
enabled: root.swapAdaptor.validSwapProposalReceived &&
|
|
editSlippagePanel.valid &&
|
|
!payPanel.amountEnteredGreaterThanBalance &&
|
|
!root.swapAdaptor.approvalPending
|
|
onClicked: {
|
|
if (root.swapAdaptor.validSwapProposalReceived) {
|
|
if (root.swapAdaptor.swapOutputData.approvalNeeded)
|
|
Global.openPopup(swapApproveModalComponent)
|
|
else
|
|
Global.openPopup(swapSignModalComponent)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: swapApproveModalComponent
|
|
SwapApproveCapModal {
|
|
destroyOnClose: true
|
|
|
|
loginType: root.swapAdaptor.selectedAccount.migratedToKeycard ? Constants.LoginType.Keycard : root.loginType
|
|
feesLoading: root.swapAdaptor.swapProposalLoading
|
|
|
|
fromTokenSymbol: root.swapAdaptor.fromToken.symbol
|
|
fromTokenAmount: SQUtils.AmountsArithmetic.div(
|
|
SQUtils.AmountsArithmetic.fromString(root.swapAdaptor.swapOutputData.approvalAmountRequired),
|
|
SQUtils.AmountsArithmetic.fromNumber(1, root.swapAdaptor.fromToken.decimals ?? 18)).toFixed()
|
|
fromTokenContractAddress: SQUtils.ModelUtils.getByKey(root.swapAdaptor.fromToken.addressPerChain,
|
|
"chainId", root.swapInputParamsForm.selectedNetworkChainId,
|
|
"address")
|
|
|
|
accountName: root.swapAdaptor.selectedAccount.name
|
|
accountAddress: root.swapAdaptor.selectedAccount.address
|
|
accountEmoji: root.swapAdaptor.selectedAccount.emoji
|
|
accountColor: Utils.getColorForId(root.swapAdaptor.selectedAccount.colorId)
|
|
accountBalanceFormatted: payPanel.accountBalanceFormatted // FIXME https://github.com/status-im/status-desktop/issues/15554
|
|
|
|
networkShortName: networkFilter.singleSelectionItemData.shortName
|
|
networkName: networkFilter.singleSelectionItemData.chainName
|
|
networkIconPath: Style.svg(networkFilter.singleSelectionItemData.iconUrl)
|
|
networkBlockExplorerUrl: networkFilter.singleSelectionItemData.blockExplorerURL
|
|
|
|
fiatFees: {
|
|
const feesInFloat = root.swapAdaptor.currencyStore.getFiatValue(root.swapAdaptor.swapOutputData.approvalGasFees, Constants.ethToken)
|
|
return root.swapAdaptor.currencyStore.formatCurrencyAmount(feesInFloat, root.swapAdaptor.currencyStore.currentCurrency)
|
|
}
|
|
cryptoFees: root.swapAdaptor.currencyStore.formatCurrencyAmount(root.swapAdaptor.swapOutputData.approvalGasFees, Constants.ethToken)
|
|
estimatedTime: root.swapAdaptor.swapOutputData.estimatedTime
|
|
|
|
serviceProviderName: root.swapAdaptor.swapOutputData.txProviderName
|
|
// serviceProviderURL: "" // FIXME get the service provider URL from backend
|
|
// serviceProviderIcon: "" // FIXME get the service icon from backend
|
|
serviceProviderContractAddress: root.swapAdaptor.swapOutputData.approvalContractAddress
|
|
|
|
onAccepted: {
|
|
root.swapAdaptor.sendApproveTx()
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: swapSignModalComponent
|
|
SwapSignModal {
|
|
destroyOnClose: true
|
|
|
|
loginType: root.swapAdaptor.selectedAccount.migratedToKeycard ? Constants.LoginType.Keycard : root.loginType
|
|
feesLoading: root.swapAdaptor.swapProposalLoading
|
|
|
|
fromTokenSymbol: root.swapAdaptor.fromToken.symbol
|
|
fromTokenAmount: root.swapInputParamsForm.fromTokenAmount
|
|
fromTokenContractAddress: SQUtils.ModelUtils.getByKey(root.swapAdaptor.fromToken.addressPerChain,
|
|
"chainId", root.swapInputParamsForm.selectedNetworkChainId,
|
|
"address")
|
|
|
|
toTokenSymbol: root.swapAdaptor.toToken.symbol
|
|
toTokenAmount: root.swapAdaptor.swapOutputData.toTokenAmount
|
|
toTokenContractAddress: SQUtils.ModelUtils.getByKey(root.swapAdaptor.toToken.addressPerChain,
|
|
"chainId", root.swapInputParamsForm.selectedNetworkChainId,
|
|
"address")
|
|
|
|
accountName: root.swapAdaptor.selectedAccount.name
|
|
accountAddress: root.swapAdaptor.selectedAccount.address
|
|
accountEmoji: root.swapAdaptor.selectedAccount.emoji
|
|
accountColor: Utils.getColorForId(root.swapAdaptor.selectedAccount.colorId)
|
|
|
|
networkShortName: networkFilter.singleSelectionItemData.shortName
|
|
networkName: networkFilter.singleSelectionItemData.chainName
|
|
networkIconPath: Style.svg(networkFilter.singleSelectionItemData.iconUrl)
|
|
networkBlockExplorerUrl: networkFilter.singleSelectionItemData.blockExplorerURL
|
|
|
|
fiatFees: root.swapAdaptor.currencyStore.formatCurrencyAmount(root.swapAdaptor.swapOutputData.totalFees,
|
|
root.swapAdaptor.currencyStore.currentCurrency)
|
|
cryptoFees: {
|
|
const cryptoValue = root.swapAdaptor.currencyStore.getCryptoValue(root.swapAdaptor.swapOutputData.totalFees, Constants.ethToken)
|
|
return root.swapAdaptor.currencyStore.formatCurrencyAmount(cryptoValue, Constants.ethToken)
|
|
}
|
|
slippage: root.swapInputParamsForm.selectedSlippage
|
|
|
|
serviceProviderName: root.swapAdaptor.swapOutputData.txProviderName
|
|
// serviceProviderURL: "" // FIXME get the service provider URL from backend
|
|
|
|
onAccepted: {
|
|
root.swapAdaptor.sendSwapTx()
|
|
root.close()
|
|
}
|
|
}
|
|
}
|
|
}
|