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

589 lines
28 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 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() {
let recipientAddress = Utils.isValidAddress(popup.addressText) ? popup.addressText : d.resolvedENSAddress
let success = false
d.isPending = true
success = popup.store.transfer(
popup.selectedAccount.address,
recipientAddress,
assetSelector.selectedAsset.symbol,
amountToSendInput.text,
gasSelector.selectedGasLimit,
gasSelector.suggestedFees.eip1559Enabled ? "" : gasSelector.selectedGasPrice,
gasSelector.selectedTipLimit,
gasSelector.selectedOverallLimit,
transactionSigner.enteredPassword,
networkSelector.selectedNetwork.chainId,
d.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
}
})
enum StackGroup {
SendDetailsGroup = 0,
AuthenticationGroup = 1
}
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
readonly property bool recipientReady: (isAddressValid || isENSValid) && !recipientSelector.isPending
readonly property bool isAddressValid: Utils.isValidAddress(recipientSelector.input.text)
property bool isENSValid: false
readonly property var resolveENS: Backpressure.debounce(popup, 500, function (ensName) {
store.resolveENS(ensName)
})
property string resolvedENSAddress
onIsReadyChanged: {
if(!isReady && isLastGroup)
stack.currentIndex = SendModal.StackGroup.SendDetailsGroup
}
readonly property string uuid: Utils.uuid()
readonly property bool isLastGroup: stack.currentIndex === (stack.count - 1)
property bool isPending: false
}
width: 556
topMargin: 64 + header.height
padding: 0
background: StatusDialogBackground {
color: Theme.palette.baseColor3
}
onSelectedAccountChanged: popup.recalculateRoutesAndFees()
onOpened: {
amountToSendInput.input.edit.forceActiveFocus()
if(popup.launchedFromChat) {
recipientSelector.input.edit.readOnly = true
recipientSelector.input.text = popup.preSelectedRecipient.name
}
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)
}
}
StackLayout {
id: stack
anchors.fill: parent
currentIndex: 0
clip: true
ColumnLayout {
id: group1
Layout.preferredWidth: parent.width
spacing: Style.current.padding
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: assetAndAmmountSelector.height + Style.current.padding
color: Theme.palette.baseColor3
layer.enabled: scrollView.contentY > -8
layer.effect: DropShadow {
verticalOffset: 2
radius: 16
samples: 17
color: Theme.palette.dropShadow
}
Column {
id: assetAndAmmountSelector
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Style.current.xlPadding
anchors.rightMargin: Style.current.xlPadding
z: 1
Row {
spacing: 16
StatusBaseText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Send")
font.pixelSize: 15
color: Theme.palette.directColor1
}
StatusListItemTag {
height: 22
width: childrenRect.width
title: assetSelector.selectedAsset.totalBalance > 0 ? qsTr("Max: %1").arg(assetSelector.selectedAsset ? d.maxFiatBalance : "0.00") : qsTr("No balances active")
closeButtonVisible: false
titleText.font.pixelSize: 12
color: d.errorMode ? Theme.palette.dangerColor2 : Theme.palette.primaryColor3
titleText.color: d.errorMode ? Theme.palette.dangerColor1 : Theme.palette.primaryColor1
}
}
Item {
width: parent.width
height: amountToSendInput.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 %1".arg(assetSelector.selectedAsset.symbol)
input.edit.color: d.errorMode ? Theme.palette.dangerColor1 : Theme.palette.directColor1
validators: [
StatusFloatValidator{
id: floatValidator
bottom: 0
top: d.maxFiatBalance
errorMessage: ""
}
]
Keys.onReleased: {
let amount = amountToSendInput.text.trim()
if (!Utils.containsOnlyDigits(amount) || 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 "%1 %2".arg(Utils.toLocaleString(currencyBalance.toFixed(2), popup.store.locale, {"currency": true})).arg(popup.store.currentCurrency.toUpperCase())
}
tokenAssetSourceFn: function (symbol) {
return symbol ? Style.png("tokens/%1".arg(symbol)) : defaultToken
}
searchTokenSymbolByAddressFn: 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()
}
}
}
StatusInput {
id: txtFiatBalance
anchors.left: parent.left
anchors.leftMargin: -12
leftPadding: 0
rightPadding: 0
font.weight: Font.Medium
font.pixelSize: 13
input.placeholderFont.pixelSize: 13
input.leftPadding: 0
input.rightPadding: 0
input.topPadding: 0
input.bottomPadding: 0
input.edit.padding: 0
input.background.color: "transparent"
input.background.border.width: 0
input.edit.color: txtFiatBalance.input.edit.activeFocus ? Theme.palette.directColor1 : Theme.palette.baseColor1
text: "0.00"
placeholderText: "0.00"
input.implicitHeight: 15
implicitWidth: txtFiatBalance.input.edit.contentWidth + 50
input.rightComponent: StatusBaseText {
id: currencyText
text: popup.store.currentCurrency.toUpperCase()
font.pixelSize: 13
color: Theme.palette.directColor5
}
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)
}
}
}
}
StatusScrollView {
id: scrollView
Layout.fillHeight: true
Layout.preferredWidth: parent.width
contentHeight: layout.height
contentWidth: parent.width
z: 0
objectName: "sendModalScroll"
Column {
id: layout
width: scrollView.availableWidth
spacing: Style.current.halfPadding
anchors.left: parent.left
StatusInput {
id: recipientSelector
property bool isPending: false
width: parent.width
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Style.current.bigPadding
anchors.rightMargin: Style.current.bigPadding
label: qsTr("To")
placeholderText: qsTr("Enter an ENS name or address")
input.background.color: Theme.palette.indirectColor1
input.background.border.width: 0
input.implicitHeight: 56
input.clearable: true
multiline: false
input.rightComponent: RowLayout {
StatusButton {
visible: recipientSelector.text === ""
borderColor: Theme.palette.primaryColor1
size: StatusBaseButton.Size.Tiny
text: qsTr("Paste")
onClicked: recipientSelector.input.edit.paste()
}
StatusFlatRoundButton {
visible: recipientSelector.text !== ""
type: StatusFlatRoundButton.Type.Secondary
Layout.preferredWidth: 24
Layout.preferredHeight: 24
icon.name: "clear"
icon.width: 16
icon.height: 16
icon.color: Theme.palette.baseColor1
backgroundHoverColor: "transparent"
onClicked: recipientSelector.input.edit.clear()
}
}
Keys.onReleased: {
if(!d.isAddressValid) {
isPending = true
Qt.callLater(d.resolveENS, input.edit.text)
}
}
}
Connections {
target: store.mainModuleInst
onResolvedENS: {
recipientSelector.isPending = false
if(Utils.isValidAddress(resolvedAddress)) {
d.resolvedENSAddress = resolvedAddress
d.isENSValid = true
}
}
}
TabAddressSelectorView {
id: addressSelector
width: parent.width
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Style.current.bigPadding
anchors.rightMargin: Style.current.bigPadding
store: popup.store
onContactSelected: {
recipientSelector.input.text = address
}
visible: !d.recipientReady
}
NetworkSelector {
id: networkSelector
width: parent.width
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Style.current.bigPadding
anchors.rightMargin: Style.current.bigPadding
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)
visible: d.recipientReady
}
Rectangle {
id: fees
radius: 13
color: Theme.palette.indirectColor1
height: text.height + gasSelector.height + gasValidator.height + Style.current.xlPadding
width: parent.width
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Style.current.bigPadding
anchors.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
asset.name: "fees"
}
Column {
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
Layout.preferredWidth: fees.width - feesIcon.width - Style.current.xlPadding
StatusBaseText {
id: text
width: 410
font.pixelSize: 15
font.weight: Font.Medium
color: Theme.palette.directColor1
text: qsTr("Fees")
wrapMode: Text.WordWrap
}
GasSelector {
id: gasSelector
width: parent.width
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 &&
popup.addressText && assetSelector.selectedAsset &&
assetSelector.selectedAsset.symbol && amountToSendInput.text)) {
selectedGasLimit = 250000
defaultGasLimit = selectedGasLimit
return
}
var chainID = networkSelector.selectedNetwork ? networkSelector.selectedNetwork.chainId: 1
var recipientAddress = popup.launchedFromChat ? popup.preSelectedRecipient.address : popup.addressText
let gasEstimate = JSON.parse(popup.store.estimateGas(
popup.selectedAccount.address,
recipientAddress,
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
width: parent.width
selectedAccount: popup.selectedAccount
selectedAmount: amountToSendInput.text === "" ? 0.0 :
parseFloat(amountToSendInput.text)
selectedAsset: assetSelector.selectedAsset
selectedGasEthValue: gasSelector.selectedGasEthValue
selectedNetwork: networkSelector.selectedNetwork ? networkSelector.selectedNetwork: null
}
}
}
}
}
}
}
Column{
id: group2
Layout.preferredWidth: parent.width
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
pending: d.isPending
isLastGroup: d.isLastGroup
visible: d.isReady && !isNaN(parseFloat(amountToSendInput.text)) && gasValidator.isValid
onNextButtonClicked: {
if (isLastGroup) {
return popup.sendTransaction()
}
if(gasSelector.suggestedFees.eip1559Enabled && 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.currentIndex = SendModal.StackGroup.AuthenticationGroup
}
})
return
}
}
stack.currentIndex = SendModal.StackGroup.AuthenticationGroup
}
}
Component {
id: transactionSettingsConfirmationPopupComponent
TransactionSettingsConfirmationPopup {}
}
Connections {
target: popup.store.walletSectionTransactionsInst
onTransactionSent: {
d.isPending = false
try {
let response = JSON.parse(txResult)
if (response.uuid !== d.uuid) return
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()
// }
}
}