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

485 lines
21 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 var store
property var contactsStore
property var selectedAccount: store.currentAccount
property var preSelectedRecipient
property bool launchedFromChat: false
property MessageDialog sendingError: MessageDialog {
id: sendingError
//% "Error sending the transaction"
title: qsTrId("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() {
networkSelector.suggestedRoutes = popup.store.suggestedRoutes(
popup.selectedAccount.address, amountToSendInput.text, assetSelector.selectedAsset.symbol
)
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
onIsReadyChanged: {
if(!isReady && stack.isLastGroup)
stack.back()
}
}
width: 556
height: 595
showHeader: false
showFooter: false
showAdvancedFooter: d.isReady && gasValidator.isValid
showAdvancedHeader: true
onSelectedAccountChanged: popup.recalculateRoutesAndFees()
onOpened: {
amountToSendInput.input.edit.forceActiveFocus()
assetSelector.assets = Qt.binding(function() {
if (popup.selectedAccount) {
return popup.selectedAccount.assets
}
})
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.leftMargin: Style.current.xlPadding
anchors.topMargin: Style.current.xlPadding
anchors.rightMargin: Style.current.xlPadding
anchors.bottomMargin: popup.showAdvancedFooter && !!advancedFooter ? advancedFooter.height : Style.current.padding
TransactionFormGroup {
id: group1
anchors.fill: parent
ColumnLayout {
id: assetAndAmmountSelector
anchors.top: parent.top
anchors.right: parent.right
anchors.left: parent.left
RowLayout {
spacing: 16
StatusBaseText {
//% "Send"
text: qsTrId("command-button-send")
font.pixelSize: 15
color: Theme.palette.directColor1
Layout.alignment: Qt.AlignVCenter
}
StatusListItemTag {
//% "No balances active"
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
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
AmountInputWithCursor {
id: amountToSendInput
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: parent.width - assetSelector.width
input.placeholderText: "0.00" + " " + assetSelector.selectedAsset.symbol
errorMessageCmp.anchors.rightMargin: -100
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
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.leftMargin: -Style.current.xlPadding
width: popup.width
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
}
ScrollView {
id: scrollView
height: stack.height - assetAndAmmountSelector.height
width: parent.width
anchors.top: border.bottom
anchors.topMargin: Style.current.halfPadding
anchors.left: parent.left
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentHeight: recipientSelector.height + addressSelector.height + networkSelector.height + gasSelector.height + gasValidator.height
clip: true
// To-do use standard StatusInput component once the flow for ens name resolution is clear
RecipientSelector {
anchors.top: assetAndAmmountSelector.bottom
anchors.topMargin: Style.current.halfPadding
anchors.right: parent.right
anchors.left: parent.left
id: recipientSelector
accounts: popup.store.accounts
contactsStore: popup.contactsStore
//% To
label: qsTr("To")
Layout.fillWidth: true
//% "Enter an ENS name or address"
input.placeholderText: qsTr("Enter an ENS name or address")
input.anchors.leftMargin: 0
input.anchors.rightMargin: 0
labelFont.pixelSize: 15
labelFont.weight: Font.Normal
input.height: 56
isSelectorVisible: false
addContactEnabled: false
onSelectedRecipientChanged: gasSelector.estimateGas()
}
TabAddressSelectorView {
id: addressSelector
anchors.top: recipientSelector.bottom
anchors.right: parent.right
anchors.left: parent.left
store: popup.store
onContactSelected: {
recipientSelector.input.text = address
}
}
NetworkSelector {
id: networkSelector
anchors.top: addressSelector.bottom
anchors.right: parent.right
anchors.left: parent.left
onNetworkChanged: function(chainId) {
gasSelector.suggestedFees = popup.store.suggestedFees(chainId)
gasSelector.updateGasEthValue()
}
}
GasSelector {
id: gasSelector
anchors.top: networkSelector.bottom
getGasEthValue: popup.store.getGasEthValue
getFiatValue: popup.store.getFiatValue
getEstimatedTime: popup.store.getEstimatedTime
defaultCurrency: popup.store.currentCurrency
chainId: networkSelector.selectedNetwork.chainId
width: stack.width
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
}
let gasEstimate = JSON.parse(popup.store.estimateGas(
popup.selectedAccount.address,
recipientSelector.selectedRecipient.address,
assetSelector.selectedAsset.symbol,
amountToSendInput.text,
networkSelector.selectedNetwork.chainId || Global.currentChainId,
""))
if (!gasEstimate.success) {
//% "Error estimating gas: %1"
console.warn(qsTrId("error-estimating-gas---1").arg(gasEstimate.error.message))
return
}
selectedGasLimit = gasEstimate.result
defaultGasLimit = selectedGasLimit
})
}
GasValidator {
id: gasValidator
anchors.top: gasSelector.bottom
selectedAccount: popup.selectedAccount
selectedAmount: amountToSendInput.text === "" ? 0.0
: parseFloat(amountToSendInput.text)
selectedAsset: assetSelector.selectedAsset
selectedGasEthValue: gasSelector.selectedGasEthValue
selectedNetwork: networkSelector.selectedNetwork
}
}
}
TransactionFormGroup {
id: group4
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)){
//% "Wrong password"
transactionSigner.validationError = qsTrId("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 = qsTrId("transaction-completed")
// Global.toastMessage.source = Style.svg("check-circle")
// Global.toastMessage.iconColor = Style.current.success
// } else {
// //% "Transaction failed"
// Global.toastMessage.title = qsTrId("ens-registration-failed-title")
// Global.toastMessage.source = Style.svg("block-icon")
// Global.toastMessage.iconColor = Style.current.danger
// }
// Global.toastMessage.link = `${walletModel.utilsView.etherscanLink}/${txHash}`
// Global.toastMessage.open()
// }
}
}