diff --git a/src/app/profile/views/ens_manager.nim b/src/app/profile/views/ens_manager.nim index 4bee331a67..afbebf5301 100644 --- a/src/app/profile/views/ens_manager.nim +++ b/src/app/profile/views/ens_manager.nim @@ -6,7 +6,10 @@ import sequtils from ../../../status/libstatus/types import Setting import ../../../status/threads import ../../../status/ens as status_ens +import ../../../status/libstatus/wallet as status_wallet import ../../../status/libstatus/settings as status_settings +import ../../../status/libstatus/utils as utils +import ../../../status/libstatus/tokens as tokens import ../../../status/status type @@ -130,3 +133,12 @@ QtObject: { EnsRoles.UserName.int:"username" }.toTable + + proc getPrice(self: EnsManager): string {.slot.} = + result = utils.wei2Eth(getPrice()) + + proc getUsernameRegistrar(self: EnsManager): string {.slot.} = + result = statusRegistrarAddress() + + proc getENSRegistry(self: EnsManager): string {.slot.} = + result = registry diff --git a/src/app/wallet/view.nim b/src/app/wallet/view.nim index 957521ea02..007f22f391 100644 --- a/src/app/wallet/view.nim +++ b/src/app/wallet/view.nim @@ -2,6 +2,7 @@ import NimQml, Tables, strformat, strutils, chronicles, json, std/wrapnils, pars import ../../status/[status, wallet, threads] import ../../status/wallet/collectibles as status_collectibles import ../../status/libstatus/wallet as status_wallet +import ../../status/libstatus/tokens import ../../status/libstatus/utils import views/[asset_list, account_list, account_item, transaction_list, collectibles_list] @@ -430,3 +431,9 @@ QtObject: QtProperty[string] defaultGasLimit: read = defaultGasLimit + proc getSNTBalance*(self: WalletView): string {.slot.} = + let currAcct = status_wallet.getWalletAccounts()[0] + result = getSNTBalance($currAcct.address) + + proc getDefaultAddress*(self: WalletView): string {.slot.} = + result = $status_wallet.getWalletAccounts()[0].address diff --git a/src/status/ens.nim b/src/status/ens.nim index fa62c63d22..9dd5b6142e 100644 --- a/src/status/ens.nim +++ b/src/status/ens.nim @@ -2,12 +2,17 @@ import strutils import profile/profile import nimcrypto import json +import json_serialization +import tables import strformat import libstatus/core +import libstatus/types +import libstatus/utils import stew/byteutils import unicode import algorithm - +import eth/common/eth_types, stew/byteutils +import libstatus/contracts const domain* = ".stateofus.eth" proc userName*(ensName: string, removeSuffix: bool = false): string = @@ -46,7 +51,7 @@ proc namehash*(ensName:string): string = result = "0x" & node.toHex() -const registry = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" +const registry* = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" const resolver_signature = "0x0178b8bf" proc resolver*(usernameHash: string): string = let payload = %* [{ @@ -110,4 +115,51 @@ proc address*(username: string): string = let address = response.parseJson["result"].getStr; if address == "0x0000000000000000000000000000000000000000000000000000000000000000": return "" - result = "0x" & address.substr(26) \ No newline at end of file + result = "0x" & address.substr(26) + +proc getPrice*(): Stuint[256] = + let + contract = contracts.getContract("ens-usernames") + payload = %* [{ + "to": $contract.address, + "data": contract.methods["getPrice"].encodeAbi() + }, "latest"] + + let responseStr = callPrivateRPC("eth_call", payload) + let response = Json.decode(responseStr, RpcResponse) + if not response.error.isNil: + raise newException(RpcException, "Error getting ens username price: " & response.error.message) + if response.result == "0x": + raise newException(RpcException, "Error getting ens username price: 0x") + result = fromHex(Stuint[256], response.result) + +proc registerUsername*(username:string, address: EthAddress, pubKey: string, price: Stuint[256], password: string): string = + let label = fromHex(FixedBytes[32], namehash(addDomain(username))) + let x = fromHex(FixedBytes[32], "0x" & pubkey[4..67]) + let y = fromHex(FixedBytes[32], "0x" & pubkey[68..131]) + let + ensUsernamesContract = contracts.getContract("ens-usernames") + sntContract = contracts.getContract("snt") + price = getPrice() + register = Register(label: label, account: address, x: x, y: y) + registerAbiEncoded = ensUsernamesContract.methods["register"].encodeAbi(register) + + let + approveAndCallObj = ApproveAndCall(to: ensUsernamesContract.address, value: price, data: DynamicBytes[136].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) + result = response.result # should be a tx receipt + +proc statusRegistrarAddress*():string = + result = $contracts.getContract("ens-usernames").address diff --git a/src/status/libstatus/coder.nim b/src/status/libstatus/coder.nim index 0aee5d5016..d19ce034cc 100644 --- a/src/status/libstatus/coder.nim +++ b/src/status/libstatus/coder.nim @@ -26,10 +26,17 @@ type address*: EthAddress price*: Stuint[256] + Register* = object + label*: FixedBytes[32] + account*: EthAddress + x*: FixedBytes[32] + y*: FixedBytes[32] + + ApproveAndCall* = object to*: EthAddress value*: Stuint[256] - data*: DynamicBytes[100] + data*: DynamicBytes[136] Transfer* = object to*: EthAddress diff --git a/src/status/libstatus/contracts.nim b/src/status/libstatus/contracts.nim index 0321d54dd6..c538bc5989 100644 --- a/src/status/libstatus/contracts.nim +++ b/src/status/libstatus/contracts.nim @@ -4,8 +4,8 @@ from eth/common/utils import parseAddress import ./types, ./settings, ./coder export - GetPackData, PackData, BuyToken, ApproveAndCall, Transfer, BalanceOf, - TokenOfOwnerByIndex, TokenPackId, TokenUri, DynamicBytes, toHex, fromHex + GetPackData, PackData, BuyToken, ApproveAndCall, Transfer, BalanceOf, Register, + TokenOfOwnerByIndex, TokenPackId, TokenUri, FixedBytes, DynamicBytes, toHex, fromHex type Method* = object name*: string @@ -90,6 +90,18 @@ proc allContracts(): seq[Contract] = @[ ].toTable ), Contract(name: "crypto-kitties", network: Network.Mainnet, address: parseAddress("0x06012c8cf97bead5deae237070f9587f8e7a266d")), + Contract(name: "ens-usernames", network: Network.Mainnet, address: parseAddress("0xDB5ac1a559b02E12F29fC0eC0e37Be8E046DEF49"), + methods: [ + ("register", Method(signature: "register(bytes32,address,bytes32,bytes32)")), + ("getPrice", Method(signature: "getPrice()")) + ].toTable + ), + Contract(name: "ens-usernames", network: Network.Testnet, address: parseAddress("0x11d9F481effd20D76cEE832559bd9Aca25405841"), + methods: [ + ("register", Method(signature: "register(bytes32,address,bytes32,bytes32)")), + ("getPrice", Method(signature: "getPrice()")) + ].toTable + ), ] proc getContract(network: Network, name: string): Contract = diff --git a/src/status/libstatus/stickers.nim b/src/status/libstatus/stickers.nim index 17e61df074..7ea515c471 100644 --- a/src/status/libstatus/stickers.nim +++ b/src/status/libstatus/stickers.nim @@ -134,7 +134,7 @@ proc buyPack*(packId: Stuint[256], address: EthAddress, price: Stuint[256], pass buyToken = BuyToken(packId: packId, address: address, price: price) buyTxAbiEncoded = stickerMktContract.methods["buyToken"].encodeAbi(buyToken) let - approveAndCallObj = ApproveAndCall(to: stickerMktContract.address, value: price, data: DynamicBytes[100].fromHex(buyTxAbiEncoded)) + approveAndCallObj = ApproveAndCall(to: stickerMktContract.address, value: price, data: DynamicBytes[136].fromHex(buyTxAbiEncoded)) approveAndCallAbiEncoded = sntContract.methods["approveAndCall"].encodeAbi(approveAndCallObj) let payload = %* { "from": $address, diff --git a/src/status/libstatus/tokens.nim b/src/status/libstatus/tokens.nim index 083ee463a4..ea4113b211 100644 --- a/src/status/libstatus/tokens.nim +++ b/src/status/libstatus/tokens.nim @@ -1,5 +1,8 @@ import json, chronicles, strformat, stint, strutils import core, wallet +import contracts +import eth/common/eth_types, eth/common/utils, stew/byteutils +import json_serialization logScope: topics = "wallet" @@ -36,6 +39,10 @@ proc getTokenBalance*(tokenAddress: string, account: string): string = let balance = response.parseJson["result"].getStr result = $hex2Eth(balance) +proc getSNTBalance*(account: string): string = + let snt = contracts.getContract("snt") + result = getTokenBalance("0x" & $snt.address, account) + proc addOrRemoveToken*(enable: bool, address: string, name: string, symbol: string, decimals: int, color: string): JsonNode = if enable: addCustomToken(address, name, symbol, decimals, color) diff --git a/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml b/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml index 362af85af1..446790e754 100644 --- a/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml +++ b/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml @@ -7,6 +7,10 @@ import "../../../../../shared" Item { property var onClick: function(){} + property string username: "" + + signal backBtnClicked(); + StyledText { id: sectionTitle //% "ENS usernames" @@ -19,6 +23,124 @@ Item { font.pixelSize: 20 } + ModalPopup { + id: popup + title: qsTr("Terms of name registration") + + ScrollView { + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + width: parent.width + height: parent.height + clip: true + + Column { + spacing: Style.current.halfPadding + height: childrenRect.height + width: parent.width + + + StyledText { + text: qsTr("Funds are deposited for 1 year. Your SNT will be locked, but not spent.") + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.right: parent.right + } + + StyledText { + text: qsTr("After 1 year, you can release the name and get your deposit back, or take no action to keep the name.") + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.right: parent.right + } + + StyledText { + text: qsTr("If terms of the contract change — e.g. Status makes contract upgrades — user has the right to release the username regardless of time held.") + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.right: parent.right + } + + StyledText { + text: qsTr("The contract controller cannot access your deposited funds. They can only be moved back to the address that sent them.") + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.right: parent.right + } + + StyledText { + text: qsTr("Your address(es) will be publicly associated with your ENS name.") + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.right: parent.right + } + + StyledText { + text: qsTr("Usernames are created as subdomain nodes of stateofus.eth and are subject to the ENS smart contract terms.") + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.right: parent.right + } + + StyledText { + text: qsTr("You authorize the contract to transfer SNT on your behalf. This can only occur when you approve a transaction to authorize the transfer.") + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.right: parent.right + } + + StyledText { + text: qsTr("These terms are guaranteed by the smart contract logic at addresses:") + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.right: parent.right + font.weight: Font.Bold + } + + StyledText { + text: qsTr("%1 (Status UsernameRegistrar).").arg(profileModel.ens.getUsernameRegistrar()) + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.right: parent.right + font.family: Style.current.fontHexRegular.name + } + + StyledText { + text: qsTr(`Look up on Etherscan`).arg(walletModel.etherscanLink.replace("/tx", "/address")).arg(profileModel.ens.getUsernameRegistrar()) + anchors.left: parent.left + anchors.right: parent.right + onLinkActivated: Qt.openUrlExternally(link) + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton // we don't want to eat clicks on the Text + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + + StyledText { + text: qsTr("%1 (ENS Registry).").arg(profileModel.ens.getENSRegistry()) + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.right: parent.right + font.family: Style.current.fontHexRegular.name + } + + StyledText { + text: qsTr(`Look up on Etherscan`).arg(walletModel.etherscanLink.replace("/tx", "/address")).arg(profileModel.ens.getENSRegistry()) + anchors.left: parent.left + anchors.right: parent.right + onLinkActivated: Qt.openUrlExternally(link) + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton // we don't want to eat clicks on the Text + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + + } + } + } + ScrollView { id: sview clip: true @@ -36,19 +158,131 @@ Item { anchors.right: parent.right; anchors.left: parent.left; - StyledText { - id: title - //% "TODO: show T&C and confirmation screen for acquiring a ens username" - text: qsTrId("todo--show-t-c-and-confirmation-screen-for-acquiring-a-ens-username") + Rectangle { + id: circleAt anchors.top: parent.top anchors.topMargin: 24 + anchors.horizontalCenter: parent.horizontalCenter + width: 60 + height: 60 + radius: 120 + color: Style.current.blue + + StyledText { + text: "@" + opacity: 0.7 + font.weight: Font.Bold + font.pixelSize: 18 + color: Style.current.white + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + id: ensUsername + text: username + ".stateofus.eth" font.weight: Font.Bold - font.pixelSize: 24 + font.pixelSize: 18 + anchors.top: circleAt.bottom + anchors.topMargin: 24 anchors.left: parent.left anchors.right: parent.right horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap } + + TextWithLabel { + id: walletAddressLbl + label: qsTr("Wallet address") + text: walletModel.getDefaultAddress() + textToCopy: profileModel.profile.address + anchors.left: parent.left + anchors.leftMargin: 24 + anchors.top: ensUsername.bottom + anchors.topMargin: 24 + } + + TextWithLabel { + id: keyLbl + label: qsTr("Key") + text: { + let pubKey = profileModel.profile.pubKey; + return pubKey.substring(0, 20) + "..." + pubKey.substring(pubKey.length - 20); + } + textToCopy: profileModel.profile.pubKey + anchors.left: parent.left + anchors.leftMargin: 24 + anchors.top: walletAddressLbl.bottom + anchors.topMargin: 24 + } + + CheckBox { + id: termsAndConditionsCheckbox + anchors.top: keyLbl.bottom + anchors.topMargin: Style.current.padding + anchors.left: parent.left + anchors.leftMargin: 24 + } + + StyledText { + text: qsTr("Agree to Terms of name registration. I understand that my wallet address will be publicly connected to my username.") + anchors.left: termsAndConditionsCheckbox.right + anchors.right: parent.right + wrapMode: Text.WordWrap + anchors.top: termsAndConditionsCheckbox.top + onLinkActivated: popup.open() + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton // we don't want to eat clicks on the Text + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + } + } + + StyledButton { + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.current.padding + anchors.left: parent.left + anchors.leftMargin: Style.current.padding + label: qsTr("Back") + onClicked: backBtnClicked() + } + + Item { + anchors.top: startBtn.top + anchors.right: startBtn.left + anchors.rightMargin: Style.current.padding + width: childrenRect.width + + Image { + id: image1 + height: 50 + width: 50 + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectFit + source: "../../../../../shared/img/status-logo.png" + } + + StyledText { + id: ensPriceLbl + text: qsTr("10 SNT") + anchors.left: image1.right + anchors.leftMargin: 5 + anchors.top: image1.top + color: Style.current.textColor + font.pixelSize: 14 + } + + StyledText { + text: qsTr("Deposit") + anchors.left: image1.right + anchors.leftMargin: 5 + anchors.topMargin: 5 + anchors.top: ensPriceLbl.bottom + color: Style.current.secondaryText + font.pixelSize: 14 } } @@ -56,9 +290,10 @@ Item { id: startBtn anchors.bottom: parent.bottom anchors.bottomMargin: Style.current.padding - anchors.horizontalCenter: parent.horizontalCenter - //% "Ok" - label: qsTrId("ok") + anchors.right: parent.right + anchors.rightMargin: Style.current.padding + label: parseFloat(walletModel.getSNTBalance()) < 10 ? qsTr("Not enough SNT") : qsTr("Ok") + disabled: parseFloat(walletModel.getSNTBalance()) < 10 || !termsAndConditionsCheckbox.checked onClicked: onClick() } } \ No newline at end of file diff --git a/ui/app/AppLayouts/Profile/Sections/EnsContainer.qml b/ui/app/AppLayouts/Profile/Sections/EnsContainer.qml index 5089a54839..446701db3f 100644 --- a/ui/app/AppLayouts/Profile/Sections/EnsContainer.qml +++ b/ui/app/AppLayouts/Profile/Sections/EnsContainer.qml @@ -145,6 +145,10 @@ Item { targetState: welcomeState signal: goToWelcome } + DSM.SignalTransition { + targetState: listState + signal: back + } } } @@ -167,6 +171,7 @@ Item { if(output === "connected"){ connect(username) } else { + selectedUsername = username; next(output); } } @@ -176,9 +181,11 @@ Item { Component { id: termsAndConditions TermsAndConditions { + username: selectedUsername onClick: function(output){ next(output); } + onBackBtnClicked: back(); } }