status-desktop/ui/imports/shared/popups/SendModal.qml

520 lines
24 KiB
QML

import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtQuick.Dialogs 1.3
import QtGraphicalEffects 1.0
import StatusQ.Controls.Validators 0.1
import utils 1.0
import shared.stores 1.0
import StatusQ.Controls 0.1
import StatusQ.Popups 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import "../panels"
import "../controls"
import "../views"
StatusModal {
id: popup
property alias stack: stack
property alias addressText: recipientSelector.input.text
property var store
property var contactsStore
property var selectedAccount: store.currentAccount
property var preSelectedRecipient
property bool launchedFromChat: false
property MessageDialog sendingError: MessageDialog {
id: sendingError
title: qsTr("Error sending the transaction")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
function sendTransaction() {
stack.currentGroup.isPending = true
let success = false
success = popup.store.transfer(
popup.selectedAccount.address,
recipientSelector.selectedRecipient.address,
assetSelector.selectedAsset.symbol,
amountToSendInput.text,
gasSelector.selectedGasLimit,
gasSelector.suggestedFees.eip1559Enabled ? "" : gasSelector.selectedGasPrice,
gasSelector.selectedTipLimit,
gasSelector.selectedOverallLimit,
transactionSigner.enteredPassword,
networkSelector.selectedNetwork.chainId || Global.currentChainId,
stack.uuid,
gasSelector.suggestedFees.eip1559Enabled,
)
}
property var recalculateRoutesAndFees: Backpressure.debounce(popup, 600, function(disabledChainIds) {
if (disabledChainIds === undefined) disabledChainIds = []
networkSelector.suggestedRoutes = popup.store.suggestedRoutes(
popup.selectedAccount.address, amountToSendInput.text, assetSelector.selectedAsset.symbol, disabledChainIds
)
if (networkSelector.suggestedRoutes.length) {
networkSelector.selectedNetwork = networkSelector.suggestedRoutes[0]
gasSelector.suggestedFees = popup.store.suggestedFees(networkSelector.suggestedRoutes[0].chainId)
gasSelector.checkOptimal()
gasSelector.visible = true
} else {
networkSelector.selectedNetwork = ""
gasSelector.visible = false
}
})
QtObject {
id: d
readonly property string maxFiatBalance: Utils.stripTrailingZeros(parseFloat(assetSelector.selectedAsset.totalBalance).toFixed(4))
readonly property bool isReady: amountToSendInput.valid && !amountToSendInput.pending && recipientSelector.isValid && !recipientSelector.isPending
readonly property bool errorMode: networkSelector.suggestedRoutes && networkSelector.suggestedRoutes.length <= 0 || networkSelector.errorMode
onIsReadyChanged: {
if(!isReady && stack.isLastGroup)
stack.back()
}
}
width: 556
height: 595
showHeader: false
showFooter: false
showAdvancedFooter: d.isReady && !isNaN(parseFloat(amountToSendInput.text)) && gasValidator.isValid
showAdvancedHeader: true
backgroundColor: Theme.palette.baseColor3
onSelectedAccountChanged: popup.recalculateRoutesAndFees()
onOpened: {
amountToSendInput.input.edit.forceActiveFocus()
if(popup.launchedFromChat) {
recipientSelector.selectedType = RecipientSelector.Type.Contact
recipientSelector.readOnly = true
recipientSelector.selectedRecipient = popup.preSelectedRecipient
}
popup.recalculateRoutesAndFees()
}
hasFloatingButtons: true
advancedHeaderComponent: SendModalHeader {
model: popup.store.accounts
selectedAccount: popup.selectedAccount
onUpdatedSelectedAccount: {
popup.selectedAccount = account
}
}
TransactionStackView {
id: stack
property alias currentGroup: stack.currentGroup
anchors.topMargin: Style.current.xlPadding
anchors.bottomMargin: popup.showAdvancedFooter && !!advancedFooter ? advancedFooter.height : Style.current.padding
TransactionFormGroup {
id: group1
anchors.fill: parent
color: Theme.palette.baseColor3
ColumnLayout {
id: assetAndAmmountSelector
anchors.top: parent.top
anchors.right: parent.right
anchors.left: parent.left
anchors.leftMargin: Style.current.xlPadding
anchors.rightMargin: Style.current.xlPadding
RowLayout {
spacing: 16
StatusBaseText {
text: qsTr("Send")
font.pixelSize: 15
color: Theme.palette.directColor1
Layout.alignment: Qt.AlignVCenter
}
StatusListItemTag {
title: assetSelector.selectedAsset.totalBalance > 0 ? qsTr("Max: ") + (assetSelector.selectedAsset ? d.maxFiatBalance : "0.00") : qsTr("No balances active")
closeButtonVisible: false
titleText.font.pixelSize: 12
Layout.preferredHeight: 22
Layout.preferredWidth: childrenRect.width
color: d.errorMode ? Theme.palette.dangerColor2 : Theme.palette.primaryColor3
titleText.color: d.errorMode ? Theme.palette.dangerColor1 : Theme.palette.primaryColor1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
AmountInputWithCursor {
id: amountToSendInput
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: -Style.current.padding
width: parent.width - assetSelector.width
placeholderText: "0.00" + " " + assetSelector.selectedAsset.symbol
errorMessageCmp.anchors.rightMargin: -100
input.edit.color: d.errorMode ? Theme.palette.dangerColor1 : Theme.palette.directColor1
validators: [
StatusFloatValidator{
id: floatValidator
bottom: 0
top: d.maxFiatBalance
errorMessage: qsTr("Please enter a valid amount")
}
]
Keys.onReleased: {
let amount = amountToSendInput.text.trim()
if (isNaN(amount)) {
return
}
if (amount === "") {
txtFiatBalance.text = "0.00"
} else {
txtFiatBalance.text = popup.store.getFiatValue(amount, assetSelector.selectedAsset.symbol, popup.store.currentCurrency)
}
gasSelector.estimateGas()
popup.recalculateRoutesAndFees()
}
}
StatusAssetSelector {
id: assetSelector
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
assets: popup.selectedAccount.assets
defaultToken: Style.png("tokens/DEFAULT-TOKEN@3x")
getCurrencyBalanceString: function (currencyBalance) {
return Utils.toLocaleString(currencyBalance.toFixed(2), popup.store.locale, {"currency": true}) + " " + popup.store.currentCurrency.toUpperCase()
}
tokenAssetSourceFn: function (symbol) {
return symbol ? Style.png("tokens/" + symbol) : defaultToken
}
onSelectedAssetChanged: {
if (!assetSelector.selectedAsset) {
return
}
if (amountToSendInput.text === "" || isNaN(amountToSendInput.text)) {
return
}
txtFiatBalance.text = popup.store.getFiatValue(amountToSendInput.text, assetSelector.selectedAsset.symbol, popup.store.currentCurrency)
gasSelector.estimateGas()
popup.recalculateRoutesAndFees()
}
}
}
RowLayout {
Layout.alignment: Qt.AlignLeft
StyledTextField {
id: txtFiatBalance
color: txtFiatBalance.activeFocus ? Style.current.textColor : Style.current.secondaryText
font.weight: Font.Medium
font.pixelSize: 12
inputMethodHints: Qt.ImhFormattedNumbersOnly
text: "0.00"
selectByMouse: true
background: Rectangle {
color: Style.current.transparent
}
padding: 0
Keys.onReleased: {
let balance = txtFiatBalance.text.trim()
if (balance === "" || isNaN(balance)) {
return
}
// To-Do Not refactored yet
// amountToSendInput.text = root.getCryptoValue(balance, popup.store.currentCurrency, assetSelector.selectedAsset.symbol)
}
}
StatusBaseText {
id: currencyText
text: popup.store.currentCurrency.toUpperCase()
font.pixelSize: 13
color: Theme.palette.directColor5
}
}
}
Rectangle {
id: border
anchors.top: assetAndAmmountSelector.bottom
anchors.topMargin: Style.current.padding
anchors.left: parent.left
anchors.right: parent.right
height: 1
color: Theme.palette.directColor8
visible: false
}
DropShadow {
anchors.fill: border
horizontalOffset: 0
verticalOffset: 2
radius: 8.0
samples: 17
color: Theme.palette.directColor1
source: border
}
StatusScrollView {
id: scrollView
height: stack.height - assetAndAmmountSelector.height
width: parent.width
anchors.top: border.bottom
anchors.topMargin: Style.current.halfPadding
anchors.left: parent.left
ColumnLayout {
width: scrollView.availableWidth
spacing: Style.current.halfPadding
anchors.left: parent.left
// To-do use standard StatusInput component once the flow for ens name resolution is clear
RecipientSelector {
id: recipientSelector
accounts: popup.store.accounts
contactsStore: popup.contactsStore
label: qsTr("To")
input.placeholderText: qsTr("Enter an ENS name or address")
input.anchors.leftMargin: 0
input.anchors.rightMargin: 0
input.textField.anchors.rightMargin: 0
input.bgColor: Theme.palette.indirectColor1
labelFont.pixelSize: 15
labelFont.weight: Font.Normal
input.height: 56
inputWidth: parent.width
isSelectorVisible: false
addContactEnabled: false
onSelectedRecipientChanged: gasSelector.estimateGas()
Layout.fillWidth: true
Layout.leftMargin: Style.current.bigPadding
Layout.rightMargin: Style.current.bigPadding
}
TabAddressSelectorView {
id: addressSelector
store: popup.store
onContactSelected: {
recipientSelector.input.text = address
}
Layout.fillWidth: true
Layout.leftMargin: Style.current.bigPadding
Layout.rightMargin: Style.current.bigPadding
}
NetworkSelector {
id: networkSelector
store: popup.store
selectedAccount: popup.selectedAccount
amountToSend: isNaN(parseFloat(amountToSendInput.text)) ? 0 : parseFloat(amountToSendInput.text)
requiredGasInEth: gasSelector.selectedGasEthValue
assets: popup.selectedAccount.assets
selectedAsset: assetSelector.selectedAsset
onNetworkChanged: function(chainId) {
gasSelector.suggestedFees = popup.store.suggestedFees(chainId)
gasSelector.updateGasEthValue()
}
onReCalculateSuggestedRoute: popup.recalculateRoutesAndFees(disabledChainIds)
Layout.fillWidth: true
Layout.leftMargin: Style.current.bigPadding
Layout.rightMargin: Style.current.bigPadding
}
Rectangle {
id: fees
radius: 13
color: Theme.palette.indirectColor1
implicitHeight: gasSelector.visible || gasValidator.visible ? feesLayout.height + gasValidator.height : 0
Layout.fillWidth: true
Layout.leftMargin: Style.current.bigPadding
Layout.rightMargin: Style.current.bigPadding
RowLayout {
id: feesLayout
spacing: 10
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Style.current.padding
StatusRoundIcon {
id: feesIcon
Layout.alignment: Qt.AlignTop
radius: 8
icon.name: "fees"
}
ColumnLayout {
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
Layout.preferredWidth: fees.width - feesIcon.width - Style.current.xlPadding
GasSelector {
id: gasSelector
Layout.fillWidth: true
getGasEthValue: popup.store.getGasEthValue
getFiatValue: popup.store.getFiatValue
getEstimatedTime: popup.store.getEstimatedTime
defaultCurrency: popup.store.currentCurrency
chainId: networkSelector.selectedNetwork && networkSelector.selectedNetwork.chainId ? networkSelector.selectedNetwork.chainId : 1
property var estimateGas: Backpressure.debounce(gasSelector, 600, function() {
if (!(popup.selectedAccount && popup.selectedAccount.address &&
recipientSelector.selectedRecipient && recipientSelector.selectedRecipient.address &&
assetSelector.selectedAsset && assetSelector.selectedAsset.symbol &&
amountToSendInput.text)) {
selectedGasLimit = 250000
defaultGasLimit = selectedGasLimit
return
}
var chainID = networkSelector.selectedNetwork ? networkSelector.selectedNetwork.chainId: 1
let gasEstimate = JSON.parse(popup.store.estimateGas(
popup.selectedAccount.address,
recipientSelector.selectedRecipient.address,
assetSelector.selectedAsset.symbol,
amountToSendInput.text,
chainID || Global.currentChainId,
""))
if (!gasEstimate.success) {
console.warn("error estimating gas: ", gasEstimate.error.message)
return
}
selectedGasLimit = gasEstimate.result
defaultGasLimit = selectedGasLimit
})
}
GasValidator {
id: gasValidator
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
selectedAccount: popup.selectedAccount
selectedAmount: amountToSendInput.text === "" ? 0.0 :
parseFloat(amountToSendInput.text)
selectedAsset: assetSelector.selectedAsset
selectedGasEthValue: gasSelector.selectedGasEthValue
selectedNetwork: networkSelector.selectedNetwork ? networkSelector.selectedNetwork: null
}
}
}
}
}
}
}
TransactionFormGroup {
id: group4
color: Theme.palette.baseColor3
StackView.onActivated: {
transactionSigner.forceActiveFocus(Qt.MouseFocusReason)
}
TransactionSigner {
id: transactionSigner
Layout.topMargin: Style.current.smallPadding
width: stack.width
signingPhrase: popup.store.signingPhrase
}
}
}
advancedFooterComponent: SendModalFooter {
maxFiatFees: gasSelector.maxFiatFees
estimatedTxTimeFlag: gasSelector.estimatedTxTimeFlag
currentGroupPending: stack.currentGroup.isPending
currentGroupValid: stack.currentGroup.isValid
isLastGroup: stack.isLastGroup
onNextButtonClicked: {
const validity = stack.currentGroup.validate()
if (validity.isValid && !validity.isPending) {
if (stack.isLastGroup) {
return popup.sendTransaction()
}
if(gasSelector.suggestedFees.eip1559Enabled && stack.currentGroup === group1 && gasSelector.advancedMode){
if(gasSelector.showPriceLimitWarning || gasSelector.showTipLimitWarning){
Global.openPopup(transactionSettingsConfirmationPopupComponent, {
currentBaseFee: gasSelector.suggestedFees.baseFee,
currentMinimumTip: gasSelector.perGasTipLimitFloor,
currentAverageTip: gasSelector.perGasTipLimitAverage,
tipLimit: gasSelector.selectedTipLimit,
suggestedTipLimit: gasSelector.perGasTipLimitFloor,
priceLimit: gasSelector.selectedOverallLimit,
suggestedPriceLimit: gasSelector.suggestedFees.baseFee + gasSelector.perGasTipLimitFloor,
showPriceLimitWarning: gasSelector.showPriceLimitWarning,
showTipLimitWarning: gasSelector.showTipLimitWarning,
onConfirm: function(){
stack.next();
}
})
return
}
}
stack.next()
}
}
}
Component {
id: transactionSettingsConfirmationPopupComponent
TransactionSettingsConfirmationPopup {}
}
Connections {
target: popup.store.walletSectionTransactionsInst
onTransactionSent: {
try {
let response = JSON.parse(txResult)
if (response.uuid !== stack.uuid) return
stack.currentGroup.isPending = false
if (!response.success) {
if (Utils.isInvalidPasswordMessage(response.result)){
transactionSigner.validationError = qsTr("Wrong password")
return
}
sendingError.text = response.result
return sendingError.open()
}
let url = `${popup.store.getEtherscanLink()}/${response.result}`
Global.displayToastMessage(qsTr("Transaction pending..."),
qsTr("View on etherscan"),
"",
true,
Constants.ephemeralNotificationType.normal,
url)
popup.close()
} catch (e) {
console.error('Error parsing the response', e)
}
}
// Not Refactored Yet
// onTransactionCompleted: {
// if (success) {
// //% "Transaction completed"
// Global.toastMessage.title = qsTr("Wrong password")
// Global.toastMessage.source = Style.svg("check-circle")
// Global.toastMessage.iconColor = Style.current.success
// } else {
// //% "Transaction failed"
// Global.toastMessage.title = qsTr("Wrong password")
// Global.toastMessage.source = Style.svg("block-icon")
// Global.toastMessage.iconColor = Style.current.danger
// }
// Global.toastMessage.link = `${walletModel.utilsView.etherscanLink}/${txHash}`
// Global.toastMessage.open()
// }
}
}