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

555 lines
25 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 utils 1.0
import shared.stores 1.0
import shared.panels 1.0
import StatusQ.Controls 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls.Validators 0.1
import "../panels"
import "../controls"
import "../views"
StatusDialog {
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,
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 && recipientReady
readonly property bool errorMode: networkSelector.suggestedRoutes && networkSelector.suggestedRoutes.length <= 0 || networkSelector.errorMode
property bool recipientReady: recipientSelector.isValid && !recipientSelector.isPending
onIsReadyChanged: {
if(!isReady && stack.isLastGroup)
stack.back()
}
}
width: 556
height: 595
padding: 0
background: StatusDialogBackground {
color: 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()
}
header: SendModalHeader {
anchors.top: parent.top
anchors.topMargin: -height - 18
model: popup.store.accounts
selectedAccount: popup.selectedAccount
changeSelectedAccount: function(newIndex) {
if (newIndex > popup.store.accounts) {
return
}
popup.store.switchAccount(newIndex)
}
}
TransactionStackView {
id: stack
property alias currentGroup: stack.currentGroup
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
z: 1
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 - Style.current.bigPadding
width: parent.width
anchors.top: border.bottom
anchors.left: parent.left
z: 0
objectName: "sendModalScroll"
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
StatusButton {
anchors.right: parent.right
anchors.rightMargin: 16
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
visible: recipientSelector.input.textField.text === ""
border.width: 1
border.color: Theme.palette.primaryColor1
size: StatusBaseButton.Size.Tiny
text: qsTr("Paste")
onClicked: recipientSelector.input.textField.paste()
}
}
TabAddressSelectorView {
id: addressSelector
store: popup.store
onContactSelected: {
recipientSelector.input.text = address
}
Layout.fillWidth: true
Layout.leftMargin: Style.current.bigPadding
Layout.rightMargin: Style.current.bigPadding
visible: !d.recipientReady
}
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
visible: d.recipientReady
}
Rectangle {
id: fees
radius: 13
color: Theme.palette.indirectColor1
Layout.preferredHeight: text.height + gasSelector.height + gasValidator.height + Style.current.padding
Layout.fillWidth: true
Layout.leftMargin: Style.current.bigPadding
Layout.rightMargin: Style.current.bigPadding
visible: d.recipientReady
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
StatusBaseText {
id: text
Layout.maximumWidth: 410
font.pixelSize: 15
font.weight: Font.Medium
color: Theme.palette.directColor1
text: qsTr("Fees")
wrapMode: Text.WordWrap
}
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,
""))
if (!gasEstimate.success) {
console.warn("error estimating gas: ", gasEstimate.error.message)
return
}
selectedGasLimit = gasEstimate.result
defaultGasLimit = selectedGasLimit
})
}
GasValidator {
id: gasValidator
Layout.fillWidth: true
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
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Style.current.smallPadding
anchors.margins: 32
signingPhrase: popup.store.signingPhrase
}
}
}
footer: SendModalFooter {
maxFiatFees: gasSelector.maxFiatFees
estimatedTxTimeFlag: gasSelector.estimatedTxTimeFlag
currentGroupPending: stack.currentGroup.isPending
currentGroupValid: stack.currentGroup.isValid
isLastGroup: stack.isLastGroup
visible: d.isReady && !isNaN(parseFloat(amountToSendInput.text)) && gasValidator.isValid
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()
// }
}
}