diff --git a/src/app/profile/views/ens_manager.nim b/src/app/profile/views/ens_manager.nim index 85d65d302e..4f8d1fa546 100644 --- a/src/app/profile/views/ens_manager.nim +++ b/src/app/profile/views/ens_manager.nim @@ -4,7 +4,7 @@ import json import json_serialization import sequtils import strutils -from ../../../status/libstatus/types import Setting, PendingTransactionType +from ../../../status/libstatus/types import Setting, PendingTransactionType, RpcException import ../../../status/threads import ../../../status/ens as status_ens import ../../../status/libstatus/wallet as status_wallet @@ -186,10 +186,7 @@ QtObject: proc connectOwnedUsername(self: EnsManager, username: string, isStatus: bool) {.slot.} = var ensUsername = formatUsername(username, isStatus) - - self.usernames.add ensUsername self.add ensUsername - self.connect(ensUsername) proc revert*(self: EnsManager, trxType: PendingTransactionType, ensUsername: string, transactionHash: string, revertReason: string) = @@ -203,18 +200,33 @@ QtObject: self.endResetModel() self.transactionCompleted(false, transactionHash, ensUsername, $trxType, revertReason) - proc registerENS(self: EnsManager, username: string, password: string) {.slot.} = - let pubKey = status_settings.getSetting[string](Setting.PublicKey, "0x0") - let address = status_wallet.getWalletAccounts()[0].address - let walletAddress = parseAddress(address) - let trxHash = registerUsername(username, walletAddress, pubKey, password) - - self.transactionWasSent(trxHash) + proc getEnsRegisterAddress(self: EnsManager): QVariant {.slot.} = + newQVariant($statusRegistrarAddress()) - # TODO: handle transaction failure - var ensUsername = formatUsername(username, true) - self.pendingUsernames.incl(ensUsername) - self.add ensUsername + QtProperty[QVariant] ensRegisterAddress: + read = getEnsRegisterAddress + + proc registerENSGasEstimate(self: EnsManager, ensUsername: string, address: string): int {.slot.} = + let pubKey = status_settings.getSetting[string](Setting.PublicKey, "0x0") + try: + result = registerUsernameEstimateGas(ensUsername, address, pubKey) + except: + result = 325000 + + proc registerENS*(self: EnsManager, username: string, address: string, gas: string, gasPrice: string, password: string): string {.slot.} = + try: + let pubKey = status_settings.getSetting[string](Setting.PublicKey, "0x0") + let response = registerUsername(username, pubKey, address, gas, gasPrice, password) + result = $(%* { "result": %response }) + self.transactionWasSent(response) + + # TODO: handle transaction failure + var ensUsername = formatUsername(username, true) + self.pendingUsernames.incl(ensUsername) + self.add ensUsername + + except RpcException as e: + result = $(%* { "error": %* { "message": %e.msg }}) proc setPubKey(self: EnsManager, username: string, password: string) {.slot.} = let pubKey = status_settings.getSetting[string](Setting.PublicKey, "0x0") diff --git a/src/status/ens.nim b/src/status/ens.nim index 91ead41cbe..96e11200e3 100644 --- a/src/status/ens.nim +++ b/src/status/ens.nim @@ -7,13 +7,16 @@ import tables import strformat import libstatus/core import libstatus/types +from eth/common/utils as eth_utils import parseAddress import libstatus/utils import libstatus/wallet import stew/byteutils import unicode +import transactions import algorithm import eth/common/eth_types, stew/byteutils, stint import libstatus/eth/contracts + const domain* = ".stateofus.eth" proc userName*(ensName: string, removeSuffix: bool = false): string = @@ -142,7 +145,7 @@ proc getPrice*(): Stuint[256] = proc extractCoordinates*(pubkey: string):tuple[x: string, y:string] = result = ("0x" & pubkey[4..67], "0x" & pubkey[68..131]) -proc registerUsername*(username:string, address: EthAddress, pubKey: string, password: string): string = +proc registerUsernameEstimateGas*(username: string, address: string, pubKey: string): int = let label = fromHex(FixedBytes[32], label(username)) coordinates = extractCoordinates(pubkey) @@ -153,25 +156,41 @@ proc registerUsername*(username:string, address: EthAddress, pubKey: string, pas price = getPrice() let - register = Register(label: label, account: address, x: x, y: y) + register = Register(label: label, account: parseAddress(address), x: x, y: y) registerAbiEncoded = ensUsernamesContract.methods["register"].encodeAbi(register) approveAndCallObj = ApproveAndCall[132](to: ensUsernamesContract.address, value: price, data: DynamicBytes[132].fromHex(registerAbiEncoded)) approveAndCallAbiEncoded = sntContract.methods["approveAndCall"].encodeAbi(approveAndCallObj) - - let payload = %* { - "from": $address, - "to": $sntContract.address, - # "gas": 200000, # TODO: obtain gas price? - "data": approveAndCallAbiEncoded - } - let responseStr = sendTransaction($payload, password) - let response = Json.decode(responseStr, RpcResponse) - if not response.error.isNil: - raise newException(RpcException, "Error registering ens-username: " & response.error.message) - - trackPendingTransaction(response.result, $address, $sntContract.address, PendingTransactionType.RegisterENS, username & domain) - result = response.result + var tx = transactions.buildTokenTransaction(parseAddress(address), sntContract.address, "", "") + + try: + let response = sntContract.methods["approveAndCall"].estimateGas(tx, approveAndCallObj) + result = fromHex[int](response) + except RpcException as e: + raise + +proc registerUsername*(username, pubKey, address, gas, gasPrice, password: string): string = + let + label = fromHex(FixedBytes[32], label(username)) + coordinates = extractCoordinates(pubkey) + x = fromHex(FixedBytes[32], coordinates.x) + y = fromHex(FixedBytes[32], coordinates.y) + ensUsernamesContract = contracts.getContract("ens-usernames") + sntContract = contracts.getContract("snt") + price = getPrice() + + let + register = Register(label: label, account: parseAddress(address), x: x, y: y) + registerAbiEncoded = ensUsernamesContract.methods["register"].encodeAbi(register) + approveAndCallObj = ApproveAndCall[132](to: ensUsernamesContract.address, value: price, data: DynamicBytes[132].fromHex(registerAbiEncoded)) + + var tx = transactions.buildTokenTransaction(parseAddress(address), sntContract.address, gas, gasPrice) + + try: + result = sntContract.methods["approveAndCall"].send(tx, approveAndCallObj, password) + trackPendingTransaction(result, address, $sntContract.address, PendingTransactionType.RegisterENS, username & domain) + except RpcException as e: + raise proc setPubKey*(username:string, address: EthAddress, pubKey: string, password: string): string = var hash = namehash(username) diff --git a/ui/app/AppLayouts/Profile/Sections/Ens/RegisterENSModal.qml b/ui/app/AppLayouts/Profile/Sections/Ens/RegisterENSModal.qml new file mode 100644 index 0000000000..a68fcf9352 --- /dev/null +++ b/ui/app/AppLayouts/Profile/Sections/Ens/RegisterENSModal.qml @@ -0,0 +1,239 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 +import QtQuick.Dialogs 1.3 +import "../../../../../imports" +import "../../../../../shared" + +ModalPopup { + id: root + readonly property var asset: JSON.parse(walletModel.getStatusToken()) + property string ensUsername: "" + property string ensPrice: "10" + + property bool showBackBtn: false + title: qsTr("Authorize %1 %2").arg(Utils.stripTrailingZeros(ensPrice)).arg(asset.symbol) + + property MessageDialog sendingError: MessageDialog { + id: sendingError + title: qsTr("Error sending the transaction") + icon: StandardIcon.Critical + standardButtons: StandardButton.Ok + } + property MessageDialog sendingSuccess: MessageDialog { + id: sendingSuccess + //% "Success sending the transaction" + title: qsTrId("success-sending-the-transaction") + icon: StandardIcon.NoIcon + standardButtons: StandardButton.Ok + onAccepted: { + root.close() + } + } + + onClosed: { + stack.reset() + } + + function sendTransaction() { + let responseStr = profileModel.ens.registerENS(root.ensUsername, + selectFromAccount.selectedAccount.address, + gasSelector.selectedGasLimit, + gasSelector.selectedGasPrice, + transactionSigner.enteredPassword) + let response = JSON.parse(responseStr) + + if (response.error) { + if (response.error.message.includes("could not decrypt key with given password")){ + transactionSigner.validationError = qsTr("Wrong password") + return + } + sendingError.text = response.error.message + return sendingError.open() + } + + usernameRegistered(username); + sendingSuccess.text = qsTr("Transaction sent to the blockchain. You can watch the progress on Etherscan: %2%1").arg(response.result).arg(walletModel.etherscanLink) + sendingSuccess.open() + } + + TransactionStackView { + id: stack + height: parent.height + anchors.fill: parent + anchors.leftMargin: Style.current.padding + anchors.rightMargin: Style.current.padding + onGroupActivated: { + root.title = group.headerText + btnNext.label = group.footerText + } + TransactionFormGroup { + id: group1 + headerText: qsTr("Authorize %1 %2").arg(Utils.stripTrailingZeros(root.ensPrice)).arg(root.asset.symbol) + footerText: qsTr("Continue") + + StackView.onActivated: { + btnBack.visible = root.showBackBtn + } + + AccountSelector { + id: selectFromAccount + accounts: walletModel.accounts + selectedAccount: walletModel.currentAccount + currency: walletModel.defaultCurrency + width: stack.width + label: qsTr("Choose account") + showBalanceForAssetSymbol: root.asset.symbol + minRequiredAssetBalance: root.ensPrice + reset: function() { + accounts = Qt.binding(function() { return walletModel.accounts }) + selectedAccount = Qt.binding(function() { return walletModel.currentAccount }) + showBalanceForAssetSymbol = Qt.binding(function() { return root.asset.symbol }) + minRequiredAssetBalance = Qt.binding(function() { return root.ensPrice }) + } + onSelectedAccountChanged: gasSelector.estimateGas() + } + RecipientSelector { + id: selectRecipient + visible: false + accounts: walletModel.accounts + contacts: profileModel.addedContacts + selectedRecipient: { "address": profileModel.ens.ensRegisterAddress, "type": RecipientSelector.Type.Address } ////////////////////////////// + readOnly: true + onSelectedRecipientChanged: gasSelector.estimateGas() + } + GasSelector { + id: gasSelector + visible: false + slowestGasPrice: parseFloat(walletModel.safeLowGasPrice) + fastestGasPrice: parseFloat(walletModel.fastestGasPrice) + getGasEthValue: walletModel.getGasEthValue + getFiatValue: walletModel.getFiatValue + defaultCurrency: walletModel.defaultCurrency + 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 (!(root.ensUsername !== "" && selectFromAccount.selectedAccount)) { + selectedGasLimit = 325000 + return + } + selectedGasLimit = profileModel.ens.registerENSGasEstimate(root.ensUsername, selectFromAccount.selectedAccount.address) + }) + } + GasValidator { + id: gasValidator + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + selectedAccount: selectFromAccount.selectedAccount + selectedAsset: root.asset + selectedAmount: parseFloat(ensPrice) + selectedGasEthValue: gasSelector.selectedGasEthValue + reset: function() { + selectedAccount = Qt.binding(function() { return selectFromAccount.selectedAccount }) + selectedAsset = Qt.binding(function() { return root.asset }) + selectedAmount = Qt.binding(function() { return parseFloat(ensPrice) }) + selectedGasEthValue = Qt.binding(function() { return gasSelector.selectedGasEthValue }) + } + } + } + TransactionFormGroup { + id: group3 + headerText: qsTr("Authorize %1 %2").arg(Utils.stripTrailingZeros(root.ensPrice)).arg(root.asset.symbol) + footerText: qsTr("Sign with password") + + StackView.onActivated: { + btnBack.visible = true + } + + TransactionPreview { + id: pvwTransaction + width: stack.width + fromAccount: selectFromAccount.selectedAccount + gas: { + "value": gasSelector.selectedGasEthValue, + "symbol": "ETH", + "fiatValue": gasSelector.selectedGasFiatValue + } + toAccount: { + return selectRecipient.selectedRecipient + } + asset: root.asset + currency: walletModel.defaultCurrency + amount: { + const fiatValue = walletModel.getFiatValue(root.ensPrice || 0, root.asset.symbol, currency) + return { "value": root.ensPrice, "fiatValue": fiatValue } + } + reset: function() { + fromAccount = Qt.binding(function() { return selectFromAccount.selectedAccount }) + toAccount = Qt.binding(function() { return selectRecipient.selectedRecipient }) + asset = Qt.binding(function() { return root.asset }) + amount = Qt.binding(function() { return { "value": root.ensPrice, "fiatValue": walletModel.getFiatValue(root.ensPrice, root.asset.symbol, currency) } }) + gas = Qt.binding(function() { + return { + "value": gasSelector.selectedGasEthValue, + "symbol": "ETH", + "fiatValue": gasSelector.selectedGasFiatValue + } + }) + } + } + } + TransactionFormGroup { + id: group4 + headerText: qsTr("Send %1 %2").arg(Utils.stripTrailingZeros(root.ensPrice)).arg(root.asset.symbol) + footerText: qsTr("Sign with password") + + TransactionSigner { + id: transactionSigner + width: stack.width + signingPhrase: walletModel.signingPhrase + reset: function() { + signingPhrase = Qt.binding(function() { return walletModel.signingPhrase }) + } + } + } + } + + footer: Item { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + StyledButton { + id: btnBack + anchors.left: parent.left + //% "Back" + label: qsTrId("back") + onClicked: { + if (stack.isFirstGroup) { + return root.close() + } + stack.back() + } + } + StyledButton { + id: btnNext + anchors.right: parent.right + label: qsTr("Next") + disabled: !stack.currentGroup.isValid + onClicked: { + const isValid = stack.currentGroup.validate() + + if (stack.currentGroup.validate()) { + if (stack.isLastGroup) { + return root.sendTransaction() + } + stack.next() + } + } + } + } +} + +/*##^## +Designer { + D{i:0;autoSize:true;height:480;width:640} +} +##^##*/ + diff --git a/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml b/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml index 3dba67b4e5..759baa4971 100644 --- a/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml +++ b/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml @@ -22,33 +22,12 @@ Item { font.pixelSize: 20 } - ModalPopup { + RegisterENSModal { id: transactionDialog - title: qsTr("TODO: replace this for the transaction dialog") - - Input { - id: passwd - placeholderText: "Password" - anchors.top: parent.textToCopy - anchors.topMargin: 24 - anchors.left: parent.left - anchors.leftMargin: 24 - } - - StyledButton { - anchors.bottom: parent.bottom - anchors.bottomMargin: Style.current.padding - anchors.left: parent.left - anchors.leftMargin: Style.current.padding - label: qsTr("Ok") - onClicked: { - profileModel.ens.registerENS(username, passwd.text) - passwd.text = ""; - usernameRegistered(username); - transactionDialog.close(); - } - } - + ensUsername: username + width: 400 + height: 400 + showBackBtn: false } ModalPopup {