589 lines
28 KiB
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()
|
|
// }
|
|
}
|
|
}
|
|
|