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();
}
}