Eric Mastro ee1287b71d fix: prevent crash on generate account wrong password
Fixes #2448.

Currently, if a wrong password is entered when generating a wallet account, the app will crash due to attempting to decode a `GeneratedAccount ` from an rpc response containing only an error.

With this PR, we are detecting if an error is returned in the response, and if so, raising a StatusGoException. This exception is caught in the call chain, and translated in to a `StatusGoError` which is serialised and sent to the QML view, where it is parsed and displayed as an invalid password error in the input box.

refactor: remove string return values as error messages in wallet model
In the wallet model, we were passing back empty strings for no error, or an error as a string. This is not only confusing, but does not benefit from leaning on the compiler and strong types. One has to read the entire code to understand if a string result is returned when there is no error instead of implicitly being able to understand there is no return type.

To alleviate this, account creation fundtions that do not need to return a value have been changed to a void return type, and raise `StatusGoException` if there is an error encountered. This can be caught in the call chain and used as necessary (ie to pass to QML).

refactor: move invalid password string detection to Utils
Currently, we are reading returned view model values and checking to see if they include a known string from Status Go that means there was an invalid password used. This string was placed in the codebased in mulitple locations.

This PR moves the string check to a Utils function and updates all the references to use the function in Utils.
2021-05-14 10:27:37 -04:00

317 lines
12 KiB
QML

import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtQuick.Dialogs 1.3
import "../../../../../imports"
import "../../../../../shared"
import "../../../../../shared/status"
ModalPopup {
property var selectedAccount
property var selectedRecipient
property var selectedAsset
property var selectedAmount
property var selectedFiatAmount
property bool outgoing: true
property string trxData: ""
property alias transactionSigner: transactionSigner
property var sendTransaction: function(selectedGasLimit, selectedGasPrice, enteredPassword) {
let responseStr = walletModel.sendTransaction(selectFromAccount.selectedAccount.address,
selectRecipient.selectedRecipient.address,
root.selectedAsset.address,
root.selectedAmount,
selectedGasLimit,
selectedGasPrice,
enteredPassword,
stack.uuid)
root.close()
}
id: root
//% "Send"
title: qsTrId("command-button-send")
height: 504
property MessageDialog sendingError: MessageDialog {
id: sendingError
//% "Error sending the transaction"
title: qsTrId("error-sending-the-transaction")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
onClosed: {
stack.pop(groupPreview, StackView.Immediate)
}
TransactionStackView {
id: stack
anchors.fill: parent
anchors.leftMargin: Style.current.padding
anchors.rightMargin: Style.current.padding
initialItem: groupPreview
isLastGroup: stack.currentGroup === groupSignTx
onGroupActivated: {
root.title = group.headerText
btnNext.text = group.footerText
}
TransactionFormGroup {
id: groupSelectAcct
headerText: {
if(trxData.startsWith("0x095ea7b3")){
const approveData = JSON.parse(walletModel.decodeTokenApproval(selectedRecipient.address, trxData))
if(approveData.symbol)
//% "Authorize %1 %2"
return qsTrId("authorize--1--2").arg(approveData.amount).arg(approveData.symbol)
}
return qsTrId("command-button-send");
}
//% "Continue"
footerText: qsTrId("continue")
showNextBtn: false
onBackClicked: function() {
if(validate()) {
stack.pop()
}
}
AccountSelector {
id: selectFromAccount
accounts: walletModel.accounts
currency: walletModel.defaultCurrency
width: stack.width
selectedAccount: root.selectedAccount
//% "Choose account"
label: qsTrId("choose-account")
showBalanceForAssetSymbol: root.selectedAsset.symbol
minRequiredAssetBalance: parseFloat(root.selectedAmount)
onSelectedAccountChanged: if (isValid) { gasSelector.estimateGas() }
}
RecipientSelector {
id: selectRecipient
visible: false
accounts: walletModel.accounts
contacts: profileModel.contacts.addedContacts
selectedRecipient: root.selectedRecipient
readOnly: true
}
}
TransactionFormGroup {
id: groupSelectGas
//% "Network fee"
headerText: qsTrId("network-fee")
//% "Preview"
footerText: qsTrId("preview")
showNextBtn: false
onBackClicked: function() {
stack.pop()
}
GasSelector {
id: gasSelector
anchors.topMargin: Style.current.bigPadding
slowestGasPrice: parseFloat(walletModel.safeLowGasPrice)
fastestGasPrice: parseFloat(walletModel.fastestGasPrice)
getGasEthValue: walletModel.getGasEthValue
getFiatValue: walletModel.getFiatValue
defaultCurrency: walletModel.defaultCurrency
width: stack.width
property var estimateGas: Backpressure.debounce(gasSelector, 600, function() {
if (!(selectFromAccount.selectedAccount && selectFromAccount.selectedAccount.address &&
selectRecipient.selectedRecipient && selectRecipient.selectedRecipient.address &&
root.selectedAsset && root.selectedAsset.address &&
root.selectedAmount)) {
selectedGasLimit = 250000
return
}
let gasEstimate = JSON.parse(walletModel.estimateGas(
selectFromAccount.selectedAccount.address,
selectRecipient.selectedRecipient.address,
root.selectedAsset.address,
root.selectedAmount,
trxData))
if (!gasEstimate.success) {
//% "Error estimating gas: %1"
let message = qsTrId("error-estimating-gas---1").arg(gasEstimate.error.message)
console.warn(message)
//% ". The transaction will probably fail."
gasEstimateErrorPopup.confirmationText = message + qsTrId("--the-transaction-will-probably-fail-")
gasEstimateErrorPopup.open()
return
}
selectedGasLimit = gasEstimate.result
})
}
GasValidator {
id: gasValidator
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
selectedAccount: selectFromAccount.selectedAccount
selectedAmount: parseFloat(root.selectedAmount)
selectedAsset: root.selectedAsset
selectedGasEthValue: gasSelector.selectedGasEthValue
}
}
TransactionFormGroup {
id: groupPreview
//% "Transaction preview"
headerText: qsTrId("transaction-preview")
//% "Sign with password"
footerText: qsTrId("sign-with-password")
showBackBtn: false
onNextClicked: function() {
stack.push(groupSignTx, StackView.Immediate)
}
isValid: groupSelectAcct.isValid && groupSelectGas.isValid && pvwTransaction.isValid
TransactionPreview {
id: pvwTransaction
width: stack.width
fromAccount: selectFromAccount.selectedAccount
gas: {
"value": gasSelector.selectedGasEthValue,
"symbol": "ETH",
"fiatValue": gasSelector.selectedGasFiatValue
}
toAccount: selectRecipient.selectedRecipient
asset: root.selectedAsset
amount: { "value": root.selectedAmount, "fiatValue": root.selectedFiatAmount }
currency: walletModel.defaultCurrency
isFromEditable: false
trxData: root.trxData
isGasEditable: true
fromValid: balanceValidator.isValid
gasValid: gasValidator.isValid
onFromClicked: { stack.push(groupSelectAcct, StackView.Immediate) }
onGasClicked: { stack.push(groupSelectGas, StackView.Immediate) }
}
BalanceValidator {
id: balanceValidator
anchors.top: pvwTransaction.bottom
anchors.horizontalCenter: parent.horizontalCenter
account: selectFromAccount.selectedAccount
amount: !!root.selectedAmount ? parseFloat(root.selectedAmount) : 0.0
asset: root.selectedAsset
}
GasValidator {
id: gasValidator2
anchors.top: balanceValidator.visible ? balanceValidator.bottom : pvwTransaction.bottom
anchors.topMargin: balanceValidator.visible ? 5 : 0
anchors.horizontalCenter: parent.horizontalCenter
selectedAccount: selectFromAccount.selectedAccount
selectedAmount: parseFloat(root.selectedAmount)
selectedAsset: root.selectedAsset
selectedGasEthValue: gasSelector.selectedGasEthValue
}
}
TransactionFormGroup {
id: groupSignTx
//% "Sign with password"
headerText: qsTrId("sign-with-password")
//% "Send %1 %2"
footerText: qsTrId("send--1--2").arg(root.selectedAmount).arg(!!root.selectedAsset ? root.selectedAsset.symbol : "")
onBackClicked: function() {
stack.pop()
}
TransactionSigner {
id: transactionSigner
width: stack.width
signingPhrase: walletModel.signingPhrase
}
}
}
footer: Item {
width: parent.width
height: btnNext.height
StatusRoundButton {
id: btnBack
anchors.left: parent.left
icon.name: "arrow-right"
icon.width: 20
icon.height: 16
rotation: 180
visible: stack.currentGroup.showBackBtn
enabled: stack.currentGroup.isValid || stack.isLastGroup
onClicked: {
if (typeof stack.currentGroup.onBackClicked === "function") {
return stack.currentGroup.onBackClicked()
}
stack.back()
}
}
StatusButton {
id: btnNext
anchors.right: parent.right
//% "Next"
text: qsTrId("next")
enabled: stack.currentGroup.isValid && !stack.currentGroup.isPending
visible: stack.currentGroup.showNextBtn
onClicked: {
const validity = stack.currentGroup.validate()
if (validity.isValid && !validity.isPending) {
if (stack.isLastGroup) {
return root.sendTransaction(gasSelector.selectedGasLimit,
gasSelector.selectedGasPrice,
transactionSigner.enteredPassword)
}
if (typeof stack.currentGroup.onNextClicked === "function") {
return stack.currentGroup.onNextClicked()
}
stack.next()
}
}
}
Connections {
target: walletModel
onTransactionWasSent: {
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()
}
//% "Transaction pending..."
toastMessage.title = qsTrId("ens-transaction-pending")
toastMessage.source = "../../../../img/loading.svg"
toastMessage.iconColor = Style.current.primary
toastMessage.iconRotates = true
toastMessage.link = `${walletModel.etherscanLink}/${response.result}`
toastMessage.open()
root.close()
} catch (e) {
console.error('Error parsing the response', e)
}
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/