From 56d6ece3e9b59fd43c4925840428bd4683f7ddcb Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Fri, 2 Oct 2020 13:30:27 -0400 Subject: [PATCH] feat: enable sending an ETH transaction from the browser --- src/app/provider/view.nim | 38 +++- src/app/utilsView/view.nim | 7 +- ui/app/AppLayouts/Browser/BrowserLayout.qml | 27 ++- .../Browser/SendTransactionModal.qml | 187 ++++++++++++++++++ ui/imports/Constants.qml | 1 + ui/nim-status-client.pro | 4 + 6 files changed, 249 insertions(+), 15 deletions(-) create mode 100644 ui/app/AppLayouts/Browser/SendTransactionModal.qml diff --git a/src/app/provider/view.nim b/src/app/provider/view.nim index 57284a909c..d96edef966 100644 --- a/src/app/provider/view.nim +++ b/src/app/provider/view.nim @@ -1,5 +1,5 @@ import NimQml -import ../../status/[status, ens, chat/stickers] +import ../../status/[status, ens, chat/stickers, wallet] import ../../status/libstatus/types import ../../status/libstatus/core import ../../status/libstatus/settings as status_settings @@ -109,13 +109,37 @@ QtObject: } if SIGN_METHODS.contains(data.payload.rpcMethod): - return $ %* { # TODO: send transaction, return transaction hash, etc etc. Disabled in the meantime - "type": ResponseTypes.Web3SendAsyncCallback, - "messageId": data.messageId, - "error": { - "code": 4100 + try: + let request = data.request.parseJson + let fromAddress = request["params"][0]["from"].getStr() + let to = request["params"][0]["to"].getStr() + let value = request["params"][0]["value"].getStr() + let password = request["password"].getStr() + let selectedGasLimit = request["selectedGasLimit"].getStr() + let selectedGasPrice = request["selectedGasPrice"].getStr() + + var success: bool + # TODO make this async + let response = status.wallet.sendTransaction(fromAddress, to, value, selectedGasLimit, selectedGasPrice, password, success) + debug "Response", response, success + return $ %* { + "type": ResponseTypes.Web3SendAsyncCallback, + "messageId": data.messageId, + # TODO do we get an error code? + "error": (if response == "" or not success: newJString("web3-response-error") else: newJNull()), + "result": response + } + except Exception as e: + error "Error sending the transaction", msg = e.msg + return $ %* { + "type": ResponseTypes.Web3SendAsyncCallback, + "messageId": data.messageId, + "error": { + # TODO where does the code come from? + "code": 4100, + "message": e.msg + } } - } if ACC_METHODS.contains(data.payload.rpcMethod): let dappAddress = status_settings.getSetting[string](Setting.DappsAddress) diff --git a/src/app/utilsView/view.nim b/src/app/utilsView/view.nim index 0dd75d235c..5e378a1bbe 100644 --- a/src/app/utilsView/view.nim +++ b/src/app/utilsView/view.nim @@ -1,4 +1,4 @@ -import NimQml, os, strformat, strutils, parseUtils +import NimQml, os, strformat, strutils, parseUtils, chronicles import stint import ../../status/status import ../../status/stickers @@ -46,7 +46,10 @@ QtObject: return uintValue.toString() proc wei2Token*(self: UtilsView, wei: string, decimals: int): string {.slot.} = - return status_utils.wei2Token(wei, decimals) + 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/ui/app/AppLayouts/Browser/BrowserLayout.qml b/ui/app/AppLayouts/Browser/BrowserLayout.qml index d0d32e9387..3d6fb0ae2c 100644 --- a/ui/app/AppLayouts/Browser/BrowserLayout.qml +++ b/ui/app/AppLayouts/Browser/BrowserLayout.qml @@ -122,22 +122,31 @@ Item { } + property Component sendTransactionModalComponent: SendTransactionModal {} + + QtObject { id: provider WebChannel.id: "backend" signal web3Response(string data); - function postMessage(data){ - var request = JSON.parse(data) + function postMessage(data) { + var request; + try { + request = JSON.parse(data) + } catch (e) { + console.error("Error parsing the message data", e) + return; + } var ensAddr = urlENSDictionary[request.hostname]; - if(ensAddr){ + if (ensAddr) { request.hostname = ensAddr; } - - if(request.type === Constants.api_request){ - if(!web3Provider.hasPermission(request.hostname, request.permission)){ + + if (request.type === Constants.api_request) { + if (!web3Provider.hasPermission(request.hostname, request.permission)) { var dialog = accessDialogComponent.createObject(browserWindow); dialog.request = request; dialog.open(); @@ -145,6 +154,12 @@ Item { request.isAllowed = true; web3Response(web3Provider.postMessage(JSON.stringify(request))); } + } else if (request.type === Constants.web3SendAsyncReadOnly && + request.payload.method === "eth_sendTransaction") { + const sendDialog = sendTransactionModalComponent.createObject(browserWindow); + sendDialog.request = request; + sendDialog.open(); + walletModel.getGasPricePredictions() } else { web3Response(web3Provider.postMessage(data)); } diff --git a/ui/app/AppLayouts/Browser/SendTransactionModal.qml b/ui/app/AppLayouts/Browser/SendTransactionModal.qml new file mode 100644 index 0000000000..63dd5ccfbe --- /dev/null +++ b/ui/app/AppLayouts/Browser/SendTransactionModal.qml @@ -0,0 +1,187 @@ +import QtQuick 2.13 +import "../../../shared" +import "../../../imports" + +ModalPopup { + id: popup + + property var request: ({ + "type": "web3-send-async-read-only", + "messageId": 13, + "payload": { + "jsonrpc": "2.0", + "id": 19, + "method": "eth_sendTransaction", + "params": [{ + "to": "0x2127edab5d08b1e11adf7ae4bae16c2b33fdf74a", + "value": "0x9184e72a000", + "from": "0x2dcb8515ea98701614919cb82d30876780936a76" + }] + }, + "hostname": "ciqhsxa6udhk6tho3smjn4kloo5tb2ly4scv5yrbxgsx6wutijucqdq.infura.status.im", + "title": "DAPP" + }) + property string fromAccount: request.payload.params[0].from + property string toAccount: request.payload.params[0].to + property string value: { + // TODO get decimals + let val = utilsModel.wei2Token(request.payload.params[0].value, 18) + return val + } + + function postMessage(isAllowed) { + request.isAllowed = isAllowed; + provider.web3Response(web3Provider.postMessage(JSON.stringify(request))); + } + + onClosed: { + popup.destroy(); + } + + title: qsTr("Confirm transaction") + height: 600 + + property string passwordValidationError: "" + property bool loading: false + + function validate() { + if (passwordInput.text === "") { + //% "You need to enter a password" + passwordValidationError = qsTrId("you-need-to-enter-a-password") + } else if (passwordInput.text.length < 4) { + //% "Password needs to be 4 characters or more" + passwordValidationError = qsTrId("password-needs-to-be-4-characters-or-more") + } else { + passwordValidationError = "" + } + return passwordValidationError === "" + } + + onOpened: { + passwordInput.text = "" + passwordInput.forceActiveFocus(Qt.MouseFocusReason) + } + + Column { + spacing: Style.current.smallPadding + width: parent.width + + TextWithLabel { + label: qsTr("From") + text: fromAccount + } + + TextWithLabel { + label: qsTr("To") + text: toAccount + } + + TextWithLabel { + label: qsTr("Value") + text: popup.value + } + + Input { + id: passwordInput + //% "Enter your password…" + placeholderText: qsTrId("enter-your-password…") + //% "Password" + label: qsTrId("password") + textField.echoMode: TextInput.Password + validationError: popup.passwordValidationError + } + + GasSelector { + id: gasSelector + slowestGasPrice: parseFloat(walletModel.safeLowGasPrice) + fastestGasPrice: parseFloat(walletModel.fastestGasPrice) + getGasEthValue: walletModel.getGasEthValue + getFiatValue: walletModel.getFiatValue + defaultCurrency: walletModel.defaultCurrency + width: parent.width + reset: function() { + slowestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.safeLowGasPrice) }) + fastestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.fastestGasPrice) }) + } + property var estimateGas: Backpressure.debounce(gasSelector, 600, function() { + if (!(fromAccount && + toAccount && + // TODO support asset +// txtAmount.selectedAsset && txtAmount.selectedAsset.address && + popup.value)) return + + let gasEstimate = JSON.parse(walletModel.estimateGas( + fromAccount, + toAccount, + // TODO support other assets + Constants.zeroAddress, + popup.value)) + + if (!gasEstimate.success) { + //% "Error estimating gas: %1" + console.warn(qsTrId("error-estimating-gas---1").arg(gasEstimate.error.message)) + return + } + selectedGasLimit = gasEstimate.result + }) + } + // TODO find where to get the assets +// GasValidator { +// id: gasValidator +// selectedAccount: request.payload.params[0].from +// selectedAmount: parseFloat(txtAmount.selectedAmount) +// selectedAsset: txtAmount.selectedAsset +// selectedGasEthValue: gasSelector.selectedGasEthValue +// reset: function() { +// selectedAccount = Qt.binding(function() { return selectFromAccount.selectedAccount }) +// selectedAmount = Qt.binding(function() { return parseFloat(txtAmount.selectedAmount) }) +// selectedAsset = Qt.binding(function() { return txtAmount.selectedAsset }) +// selectedGasEthValue = Qt.binding(function() { return gasSelector.selectedGasEthValue }) +// } +// } + } + + footer: Item { + anchors.fill: parent + + StyledButton { + anchors.top: parent.top + anchors.left: parent.left + anchors.leftMargin: Style.current.padding + label: qsTr("Cancel") + disabled: loading + onClicked: { + // Do we need to send back an error? + popup.close() + } + } + + StyledButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.rightMargin: Style.current.padding + label: loading ? + //% "Loading..." + qsTrId("loading") : + qsTr("Confirm") + + disabled: loading || passwordInput.text === "" + + onClicked : { + loading = true + if (!validate()) { + return loading = false + } + + request.payload.selectedGasLimit = gasSelector.selectedGasLimit + request.payload.selectedGasPrice = gasSelector.selectedGasPrice + request.payload.password = passwordInput.text + request.payload.params[0].value = popup.value + provider.web3Response(web3Provider.postMessage(JSON.stringify(request))); + loading = false + + popup.close(); + } + } + } +} diff --git a/ui/imports/Constants.qml b/ui/imports/Constants.qml index 3e916b9065..4f630f54c8 100644 --- a/ui/imports/Constants.qml +++ b/ui/imports/Constants.qml @@ -60,6 +60,7 @@ QtObject { readonly property string api_request: "api-request" + readonly property string web3SendAsyncReadOnly: "web3-send-async-read-only" readonly property string permission_web3: "web3" readonly property string permission_contactCode: "contact-code" diff --git a/ui/nim-status-client.pro b/ui/nim-status-client.pro index 2eeec3bb51..69dc4bdf52 100644 --- a/ui/nim-status-client.pro +++ b/ui/nim-status-client.pro @@ -121,6 +121,7 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target DISTFILES += \ + app/AppLayouts/Browser/SendTransactionModal.qml \ app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatCommandButton.qml \ app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatCommandModal.qml \ app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatCommandsPopup.qml \ @@ -201,6 +202,9 @@ DISTFILES += \ fonts/InterStatus/InterStatus-ThinItalic.otf \ Theme.qml \ app/AppLayouts/Browser/BrowserLayout.qml \ + app/AppLayouts/Browser/BrowserDialog.qml \ + app/AppLayouts/Browser/DownloadView.qml \ + app/AppLayouts/Browser/FindBar.qml \ app/AppLayouts/Chat/ChatColumn.qml \ app/AppLayouts/Chat/ChatColumn/samples/MessagesData.qml \ app/AppLayouts/Chat/ChatColumn/samples/StickerData.qml \