diff --git a/src/app/utilsView/view.nim b/src/app/utilsView/view.nim index 0b01b0c7fa..792382244d 100644 --- a/src/app/utilsView/view.nim +++ b/src/app/utilsView/view.nim @@ -46,12 +46,6 @@ QtObject: let uintValue = status_utils.eth2Wei(parseFloat(eth), decimals) return uintValue.toString() - proc wei2Token*(self: UtilsView, wei: string, decimals: int): string {.slot.} = - var weiValue = wei - if(weiValue.startsWith("0x")): - weiValue = fromHex(Stuint[256], weiValue).toString() - return status_utils.wei2Token(weiValue, decimals) - proc getStickerMarketAddress(self: UtilsView): string {.slot.} = $self.status.stickers.getStickerMarketAddress diff --git a/src/app/wallet/view.nim b/src/app/wallet/view.nim index fd4bd5beac..66fa602df4 100644 --- a/src/app/wallet/view.nim +++ b/src/app/wallet/view.nim @@ -156,8 +156,8 @@ QtObject: proc setFocusedAccountByAddress*(self: WalletView, address: string) {.slot.} = if(self.accounts.rowCount() == 0): return - let index = self.accounts.getAccountindexByAddress(address) - if index == -1: return + var index = self.accounts.getAccountindexByAddress(address) + if index == -1: index = 0 let selectedAccount = self.accounts.getAccount(index) if self.focusedAccount.address == selectedAccount.address: return self.focusedAccount.setAccountItem(selectedAccount) diff --git a/src/status/libstatus/eth/contracts.nim b/src/status/libstatus/eth/contracts.nim index 27a528757c..a2ead11ad5 100644 --- a/src/status/libstatus/eth/contracts.nim +++ b/src/status/libstatus/eth/contracts.nim @@ -70,7 +70,7 @@ type symbol*: string hasIcon*: bool -proc newErc20Contract(name: string, network: Network, address: Address, symbol: string, decimals: int, hasIcon: bool): Erc20Contract = +proc newErc20Contract*(name: string, network: Network, address: Address, symbol: string, decimals: int, hasIcon: bool): Erc20Contract = Erc20Contract(name: name, network: network, address: address, methods: ERC20_METHODS.toTable, symbol: symbol, decimals: decimals, hasIcon: hasIcon) proc newErc721Contract(name: string, network: Network, address: Address, symbol: string, hasIcon: bool, addlMethods: seq[tuple[name: string, meth: Method]] = @[]): Erc721Contract = diff --git a/src/status/libstatus/utils.nim b/src/status/libstatus/utils.nim index d626f90116..bfd6f42a9d 100644 --- a/src/status/libstatus/utils.nim +++ b/src/status/libstatus/utils.nim @@ -53,27 +53,28 @@ proc eth2Wei*(eth: float, decimals: int = 18): UInt256 = proc gwei2Wei*(gwei: float): UInt256 = eth2Wei(gwei, 9) -proc wei2Eth*(input: Stuint[256]): string = - var one_eth = fromHex(Stuint[256], "DE0B6B3A7640000") +proc wei2Eth*(input: Stuint[256], decimals: int = 18): string = + var one_eth = u256(10).pow(decimals) # fromHex(Stuint[256], "DE0B6B3A7640000") var (eth, remainder) = divmod(input, one_eth) let leading_zeros = "0".repeat(($one_eth).len - ($remainder).len - 1) fmt"{eth}.{leading_zeros}{remainder}" -proc wei2Token*(input: string, decimals: int): string = +proc wei2Eth*(input: string, decimals: int): string = try: - var value = input.parse(Stuint[256]) - var p = u256(10).pow(decimals) - var i = value.div(p) - var r = value.mod(p) - var leading_zeros = "0".repeat(decimals - ($r).len) - var d = fmt"{leading_zeros}{$r}" - result = $i - if(r > 0): result = fmt"{result}.{d}" - result. trimZeros() + var input256: Stuint[256] + if input.contains("e+"): # we have a js string BN, ie 1e+21 + let + inputSplit = input.split("e+") + whole = inputSplit[0].u256 + remainder = u256(10).pow(inputSplit[1].parseInt) + input256 = whole * remainder + else: + input256 = input.u256 + result = wei2Eth(input256, decimals) except Exception as e: - error "Error parsing this wei value", input + error "Error parsing this wei value", input, msg=e.msg result = "0" diff --git a/src/status/signals/messages.nim b/src/status/signals/messages.nim index 3b42c1b370..89ef7ef4f9 100644 --- a/src/status/signals/messages.nim +++ b/src/status/signals/messages.nim @@ -1,11 +1,16 @@ import json, random, strutils, sequtils, sugar, chronicles import json_serialization import ../libstatus/accounts as status_accounts +import ../libstatus/accounts/constants as constants import ../libstatus/settings as status_settings +import ../libstatus/tokens as status_tokens import ../libstatus/types as status_types +import ../libstatus/eth/contracts as status_contracts import ../chat/[chat, message] import ../profile/[profile, devices] import types +import web3/conversions +from ../libstatus/utils import parseAddress, wei2Eth proc toMessage*(jsonMsg: JsonNode): Message @@ -201,13 +206,22 @@ proc toMessage*(jsonMsg: JsonNode): Message = message.stickerHash = jsonMsg["sticker"]["hash"].getStr if message.contentType == ContentType.Transaction: + let + allContracts = getErc20Contracts().concat(getCustomTokens()) + ethereum = newErc20Contract("Ethereum", Network.Mainnet, parseAddress(constants.ZERO_ADDRESS), "ETH", 18, true) + tokenAddress = jsonMsg["commandParameters"]["contract"].getStr + tokenContract = if tokenAddress == "": ethereum else: allContracts.getErc20ContractByAddress(parseAddress(tokenAddress)) + tokenContractStr = if tokenContract == nil: "{}" else: $(Json.encode(tokenContract)) + var weiStr = if tokenContract == nil: "0" else: wei2Eth(jsonMsg["commandParameters"]["value"].getStr, tokenContract.decimals) + weiStr.trimZeros() + # TODO find a way to use json_seralization for this. When I try, I get an error message.commandParameters = CommandParameters( id: jsonMsg["commandParameters"]["id"].getStr, fromAddress: jsonMsg["commandParameters"]["from"].getStr, address: jsonMsg["commandParameters"]["address"].getStr, - contract: jsonMsg["commandParameters"]["contract"].getStr, - value: jsonMsg["commandParameters"]["value"].getStr, + contract: tokenContractStr, + value: weiStr, transactionHash: jsonMsg["commandParameters"]["transactionHash"].getStr, commandState: jsonMsg["commandParameters"]["commandState"].getInt, signature: jsonMsg["commandParameters"]["signature"].getStr diff --git a/src/status/wallet.nim b/src/status/wallet.nim index 05e9ca16cb..4eae873772 100644 --- a/src/status/wallet.nim +++ b/src/status/wallet.nim @@ -64,6 +64,8 @@ proc delete*(self: WalletModel) = proc buildTokenTransaction(self: WalletModel, source, to, assetAddress: Address, value: float, transfer: var Transfer, contract: var Erc20Contract, gas = "", gasPrice = ""): EthSend = contract = getErc20Contract(assetAddress) + if contract == nil: + raise newException(ValueError, fmt"Could not find ERC-20 contract with address '{assetAddress}' for the current network") transfer = Transfer(to: to, value: eth2Wei(value, contract.decimals)) transactions.buildTokenTransaction(source, assetAddress, gas, gasPrice) diff --git a/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/SignTransactionModal.qml b/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/SignTransactionModal.qml index f44c17ccd2..5823895ec3 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/SignTransactionModal.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/SignTransactionModal.qml @@ -12,6 +12,7 @@ ModalPopup { property var selectedAsset property var selectedAmount property var selectedFiatAmount + property bool outgoing: true property string trxData: "" @@ -61,6 +62,7 @@ ModalPopup { onClosed: { stack.reset() + stack.pop(groupPreview, StackView.Immediate) } TransactionStackView { @@ -197,6 +199,7 @@ ModalPopup { onNextClicked: function() { stack.push(groupSignTx, StackView.Immediate) } + isValid: groupSelectAcct.isValid && groupSelectGas.isValid && gasValidator.isValid && pvwTransaction.isValid TransactionPreview { id: pvwTransaction @@ -211,6 +214,7 @@ ModalPopup { asset: root.selectedAsset amount: { "value": root.selectedAmount, "fiatValue": root.selectedFiatAmount } currency: walletModel.defaultCurrency + outgoing: root.outgoing reset: function() { fromAccount = Qt.binding(function() { return root.selectedAccount }) gas = Qt.binding(function() { @@ -224,8 +228,25 @@ ModalPopup { asset = Qt.binding(function() { return root.selectedAsset }) amount = Qt.binding(function() { return { "value": root.selectedAmount, "fiatValue": root.selectedFiatAmount } }) } - onFromClicked: stack.push(groupSelectAcct, StackView.Immediate) - onGasClicked: stack.push(groupSelectGas, StackView.Immediate) + isFromEditable: true + isGasEditable: true + onFromClicked: { stack.push(groupSelectAcct, StackView.Immediate) } + onGasClicked: { stack.push(groupSelectGas, StackView.Immediate) } + } + GasValidator { + id: gasValidator2 + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + selectedAccount: root.selectedAccount + selectedAmount: parseFloat(root.selectedAmount) + selectedAsset: root.selectedAsset + selectedGasEthValue: gasSelector.selectedGasEthValue + reset: function() { + selectedAccount = Qt.binding(function() { return root.selectedAccount }) + selectedAmount = Qt.binding(function() { return parseFloat(root.selectedAmount) }) + selectedAsset = Qt.binding(function() { return root.selectedAsset }) + selectedGasEthValue = Qt.binding(function() { return gasSelector.selectedGasEthValue }) + } } } TransactionFormGroup { diff --git a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionBubble.qml b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionBubble.qml index 2e8fe4161f..af194a18a6 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionBubble.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionBubble.qml @@ -24,53 +24,9 @@ Item { } } } - property var tokens: { - const count = walletModel.defaultTokenList.rowCount() - const toks = [] - for (var i = 0; i < count; i++) { - toks.push({ - "address": walletModel.defaultTokenList.rowData(i, 'address'), - "name": walletModel.defaultTokenList.rowData(i, 'name'), - "decimals": parseInt(walletModel.defaultTokenList.rowData(i, 'decimals'), 10), - "symbol": walletModel.defaultTokenList.rowData(i, 'symbol') - }) - } - return toks - } - property var token: { - if (commandParametersObject.contract === "") { - return { - symbol: "ETH", - name: "Ethereum", - address: Constants.zeroAddress, - decimals: 18, - hasIcon: true - } - } - - const count = root.tokens.length - for (var i = 0; i < count; i++) { - let token = root.tokens[i] - if (token.address === commandParametersObject.contract) { - return token - } - } - - return {} - } - property string tokenAmount: { - if (!commandParametersObject.value) { - return "0" - } - try { - return utilsModel.wei2Token(commandParametersObject.value.toString(), token.decimals) - } catch (e) { - console.error("Error getting the ETH value of:", commandParametersObject.value) - console.error("Error:", e.message) - return "0" - } - } - property string tokenSymbol: token.symbol + property var token: JSON.parse(commandParametersObject.contract) // TODO: handle {} + property string tokenAmount: commandParametersObject.value + property string tokenSymbol: token.symbol || "" property string fiatValue: { if (!tokenAmount || !token.symbol) { return "0" @@ -83,8 +39,8 @@ Item { switch (root.state) { case Constants.pending: case Constants.confirmed: - case Constants.transactionRequested: case Constants.addressRequested: return isCurrentUser + case Constants.transactionRequested: case Constants.declined: case Constants.transactionDeclined: case Constants.addressReceived: return !isCurrentUser @@ -92,6 +48,12 @@ Item { } } property int innerMargin: 12 + property bool isError: commandParametersObject.contract === "{}" + onTokenSymbolChanged: { + if (!!tokenSymbol) { + tokenImage.source = `../../../../img/tokens/${root.tokenSymbol}.png` + } + } id: root anchors.left: parent.left @@ -118,11 +80,17 @@ Item { StyledText { id: title color: Style.current.secondaryText - //% "↑ Outgoing transaction" - text: root.outgoing ? - qsTrId("--outgoing-transaction") : - //% "↓ Incoming transaction" - qsTrId("--incoming-transaction") + text: { + if (root.state === Constants.transactionRequested) { + let prefix = root.outgoing ? "↑ " : "↓ " + return prefix + qsTr("Transaction request") + } + return root.outgoing ? + //% "↑ Outgoing transaction" + qsTrId("--outgoing-transaction") : + //% "↓ Incoming transaction" + qsTrId("--incoming-transaction") + } font.weight: Font.Medium anchors.top: parent.top anchors.topMargin: Style.current.halfPadding @@ -140,9 +108,16 @@ Item { anchors.left: parent.left anchors.leftMargin: root.innerMargin + StyledText { + id: txtError + color: Style.current.danger + visible: root.isError + text: qsTr("Something has gone wrong") + } + Image { id: tokenImage - source: `../../../../img/tokens/${root.tokenSymbol}.png` + visible: !root.isError width: 24 height: 24 anchors.verticalCenter: parent.verticalCenter @@ -150,6 +125,7 @@ Item { StyledText { id: tokenText + visible: !root.isError color: Style.current.textColor text: `${root.tokenAmount} ${root.tokenSymbol}` anchors.left: tokenImage.right @@ -159,6 +135,7 @@ Item { StyledText { id: fiatText + visible: !root.isError color: Style.current.secondaryText text: root.fiatValue anchors.top: tokenText.bottom @@ -169,7 +146,15 @@ Item { Loader { id: bubbleLoader - active: isCurrentUser || (!isCurrentUser && !(root.state === Constants.addressRequested || root.state === Constants.transactionRequested)) + active: { + return !root.isError && ( + isCurrentUser || + (!isCurrentUser && + !(root.state === Constants.addressRequested || + root.state === Constants.transactionRequested) + ) + ) + } sourceComponent: stateBubbleComponent anchors.top: valueContainer.bottom anchors.topMargin: Style.current.halfPadding @@ -188,9 +173,11 @@ Item { Loader { id: buttonsLoader - active: (root.state === Constants.addressRequested && !root.outgoing) || + active: !root.isError && ( + (root.state === Constants.addressRequested && !root.outgoing) || (root.state === Constants.addressReceived && root.outgoing) || - (root.state === Constants.transactionRequested && !root.outgoing) + (root.state === Constants.transactionRequested && root.outgoing) + ) sourceComponent: root.outgoing ? signAndSendComponent : acceptTransactionComponent anchors.top: bubbleLoader.active ? bubbleLoader.bottom : valueContainer.bottom anchors.topMargin: bubbleLoader.active ? root.innerMargin : 20 @@ -208,7 +195,9 @@ Item { Component { id: signAndSendComponent - SendTransactionButton {} + SendTransactionButton { + outgoing: root.outgoing + } } StyledText { diff --git a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/SendTransactionButton.qml b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/SendTransactionButton.qml index fda5ed72bc..2dfa23196c 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/SendTransactionButton.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/SendTransactionButton.qml @@ -4,8 +4,10 @@ import "../../../../../../imports" import "../../ChatComponents" Item { + id: root width: parent.width height: childrenRect.height + Style.current.halfPadding + property bool outgoing: true Separator { id: separator @@ -55,15 +57,10 @@ Item { type: RecipientSelector.Type.Contact } } - selectedAsset: { - return { - name: token.name, - symbol: token.symbol, - address: commandParametersObject.contract - } - } + selectedAsset: token selectedAmount: tokenAmount selectedFiatAmount: fiatValue + outgoing: root.outgoing } } diff --git a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/StateBubble.qml b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/StateBubble.qml index ee8b41d51c..426338010d 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/StateBubble.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/StateBubble.qml @@ -13,6 +13,7 @@ Rectangle { border.width: 1 border.color: Style.current.border radius: 24 + color: Style.current.background SVGImage { id: stateImage diff --git a/ui/shared/TransactionPreview.qml b/ui/shared/TransactionPreview.qml index 74f2028b9f..b4f9cc8782 100644 --- a/ui/shared/TransactionPreview.qml +++ b/ui/shared/TransactionPreview.qml @@ -17,6 +17,14 @@ Item { property var reset: function() {} signal fromClicked signal gasClicked + // Creates a mouse area around the "from account". When clicked, triggers + // the "fromClicked" signal + property bool isFromEditable: false + // Creates a mouse area around the "network fee". When clicked, triggers + // the "gasClicked" signal + property bool isGasEditable: false + property bool isValid: true + property bool outgoing: true function resetInternal() { fromAccount = undefined @@ -24,7 +32,32 @@ Item { asset = undefined amount = undefined gas = undefined + isValid = true } + + function validate() { + let isValid = true + imgInsufficientBalance.visible = false + console.log(">>> [TransactionPreview.validate] outgoing:", outgoing) + if (outgoing && hasInsufficientBalance()) { + isValid = false + imgInsufficientBalance.visible = true + } + root.isValid = isValid + return isValid + } + + function hasInsufficientBalance() { + if (!root.asset || !root.fromAccount || !root.fromAccount.assets || !root.amount) { + return true + } + const currAcctAsset = Utils.findAssetBySymbol(root.fromAccount.assets, root.asset.symbol) + if (!currAcctAsset) return true + return currAcctAsset.value < root.amount.value + } + + onAssetChanged: validate() + onFromAccountChanged: validate() Column { id: content @@ -36,51 +69,64 @@ Item { //% "From" label: qsTrId("from") value: Item { + id: itmFromValue anchors.fill: parent anchors.verticalCenter: parent.verticalCenter - - StyledText { - font.pixelSize: 15 - height: 22 - text: root.fromAccount ? root.fromAccount.name : "" - elide: Text.ElideRight - anchors.left: parent.left - anchors.right: imgFromWallet.left - anchors.rightMargin: Style.current.halfPadding - anchors.verticalCenter: parent.verticalCenter - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter + function needsRightPadding() { + return imgInsufficientBalance.visible || fromArrow.visible } - SVGImage { - id: imgFromWallet - sourceSize.height: 18 - sourceSize.width: 18 - anchors.right: fromArrow.visible ? fromArrow.left : parent.right - anchors.rightMargin: fromArrow.visible ? Style.current.padding : 0 - anchors.verticalCenter: parent.verticalCenter - fillMode: Image.PreserveAspectFit - source: "../app/img/walletIcon.svg" - ColorOverlay { - anchors.fill: parent - source: parent - color: root.fromAccount ? root.fromAccount.iconColor : Style.current.blue - } - } - SVGImage { - id: fromArrow - width: 13 - visible: typeof root.fromClicked === "function" + Row { + spacing: Style.current.halfPadding + rightPadding: itmFromValue.needsRightPadding() ? Style.current.halfPadding : 0 anchors.right: parent.right - anchors.rightMargin: 7 anchors.verticalCenter: parent.verticalCenter - fillMode: Image.PreserveAspectFit - source: "../app/img/caret.svg" - rotation: 270 - ColorOverlay { - anchors.fill: parent - visible: parent.visible - source: parent - color: Style.current.secondaryText + + StyledText { + font.pixelSize: 15 + height: 22 + text: root.fromAccount ? root.fromAccount.name : "" + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + } + SVGImage { + id: imgFromWallet + sourceSize.height: 18 + sourceSize.width: 18 + horizontalAlignment: Image.AlignLeft + width: itmFromValue.needsRightPadding() ? (Style.current.halfPadding + sourceSize.width) : undefined // adding width to add addl spacing to image + anchors.verticalCenter: parent.verticalCenter + fillMode: Image.PreserveAspectFit + source: "../app/img/walletIcon.svg" + ColorOverlay { + anchors.fill: parent + source: parent + color: root.fromAccount ? root.fromAccount.iconColor : Style.current.blue + } + } + SVGImage { + id: imgInsufficientBalance + width: 13 + visible: false + anchors.verticalCenter: parent.verticalCenter + fillMode: Image.PreserveAspectFit + source: "../app/img/exclamation_outline.svg" + } + SVGImage { + id: fromArrow + width: 13 + visible: root.isFromEditable + anchors.verticalCenter: parent.verticalCenter + fillMode: Image.PreserveAspectFit + source: "../app/img/caret.svg" + rotation: 270 + ColorOverlay { + anchors.fill: parent + visible: parent.visible + source: parent + color: Style.current.secondaryText + } } } MouseArea { @@ -398,7 +444,7 @@ Item { SVGImage { id: gasArrow width: 13 - visible: typeof root.gasClicked === "function" + visible: root.isGasEditable anchors.right: parent.right anchors.rightMargin: 7 anchors.verticalCenter: parent.verticalCenter