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 } searchTokenSymbolByAddress: function (address) { if(popup.selectedAccount) { return popup.selectedAccount.findTokenSymbolByAddress(address) } return "" } 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 implicitHeight: 71 StatusButton { anchors.right: parent.right anchors.rightMargin: Style.current.xlPadding 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() // } } }