feat: release ENS names

This commit is contained in:
Richard Ramos 2021-08-09 18:23:52 -04:00 committed by Iuri Matias
parent ef1140d0a7
commit 80343446ec
11 changed files with 387 additions and 74 deletions

View File

@ -11,7 +11,7 @@ import ../../../status/wallet
import sets
import web3/ethtypes
import ../../../status/tasks/[qt, task_runner_impl]
import chronicles
type
EnsRoles {.pure.} = enum
UserName = UserRole + 1
@ -45,11 +45,21 @@ const detailsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
arg = decode[DetailsTaskArg](argEncoded)
address = status_ens.address(arg.username)
pubkey = status_ens.pubkey(arg.username)
json = %* {
"ensName": arg.username,
"address": address,
"pubkey": pubkey
}
var isStatus:bool = false
var expirationTime:int = 0
if arg.username.endsWith(domain):
isStatus = true
var success = false
expirationTime = status_ens.getExpirationTime(arg.username.replace(domain, ""), success)
let json = %* {
"ensName": arg.username,
"address": address,
"pubkey": pubkey,
"isStatus": isStatus,
"expirationTime": expirationTime
}
arg.finish(json)
proc details[T](self: T, slot: string, username: string) =
@ -87,8 +97,9 @@ QtObject:
let pendingTransactions = self.status.wallet.getPendingTransactions()
if (pendingTransactions == ""):
return
for trx in pendingTransactions.parseJson{"result"}.getElems():
if trx["type"].getStr == $PendingTransactionType.RegisterENS:
if trx["type"].getStr == $PendingTransactionType.RegisterENS or trx["type"].getStr == $PendingTransactionType.SetPubKey:
self.usernames.add trx["additionalData"].getStr
self.pendingUsernames.incl trx["additionalData"].getStr
@ -106,6 +117,19 @@ QtObject:
self.usernames.add(username)
self.endInsertRows()
proc remove*(self: EnsManager, username: string) =
var idx = -1
var i = 0
for u in self.usernames:
if u == username:
idx = i
break
i = i + 1
if idx == -1: return
self.beginRemoveRows(newQModelIndex(), idx, idx)
self.usernames.delete(idx)
self.endRemoveRows()
proc getPreferredUsername(self: EnsManager): string {.slot.} =
result = self.status.settings.getSetting[:string](Setting.PreferredUsername, "")
@ -138,12 +162,12 @@ QtObject:
self.loading(true)
self.details("setDetails", username)
proc detailsObtained(self: EnsManager, ensName: string, address: string, pubkey: string) {.signal.}
proc detailsObtained(self: EnsManager, ensName: string, address: string, pubkey: string, isStatus: bool, expirationTime: int) {.signal.}
proc setDetails(self: EnsManager, details: string): string {.slot.} =
self.loading(false)
let detailsJson = details.parseJson
self.detailsObtained(detailsJson["ensName"].getStr, detailsJson["address"].getStr, detailsJson["pubkey"].getStr)
self.detailsObtained(detailsJson["ensName"].getStr, detailsJson["address"].getStr, detailsJson["pubkey"].getStr, detailsJson["isStatus"].getBool, detailsJson["expirationTime"].getInt)
method rowCount(self: EnsManager, index: QModelIndex = nil): int =
return self.usernames.len
@ -228,6 +252,22 @@ QtObject:
self.pendingUsernames.incl(ensUsername)
self.add ensUsername
proc releaseEstimate(self: EnsManager, ensUsername: string, address: string): int {.slot.} =
var success: bool
result = releaseEstimateGas(ensUsername, address, success)
if not success:
result = 100000
proc release*(self: EnsManager, username: string, address: string, gas: string, gasPrice: string, password: string): string {.slot.} =
var success: bool
let response = release(username, address, gas, gasPrice, password, success)
result = $(%* { "result": %response, "success": %success })
if success:
self.transactionWasSent(response)
self.pendingUsernames.excl(username)
self.remove(username)
proc setPubKeyGasEstimate(self: EnsManager, ensUsername: string, address: string): int {.slot.} =
var success: bool
let pubKey = self.status.settings.getSetting[:string](Setting.PublicKey, "0x0")

View File

@ -16,6 +16,7 @@ import transactions
import algorithm
import web3/[ethtypes, conversions], stew/byteutils, stint
import libstatus/eth/contracts
import libstatus/eth/transactions as eth_transactions
import chronicles, libp2p/[multihash, multicodec, cid]
import ./settings as status_settings
@ -172,6 +173,54 @@ proc getPrice*(): Stuint[256] =
raise newException(RpcException, "Error getting ens username price: 0x")
result = fromHex(Stuint[256], response.result)
proc releaseEstimateGas*(username: string, address: string, success: var bool): int =
let
label = fromHex(FixedBytes[32], label(username))
ensUsernamesContract = contracts.getContract("ens-usernames")
release = Release(label: label)
var tx = transactions.buildTokenTransaction(parseAddress(address), ensUsernamesContract.address, "", "")
try:
let response = ensUsernamesContract.methods["release"].estimateGas(tx, release, success)
if success:
result = fromHex[int](response)
except RpcException as e:
raise
proc release*(username: string, address: string, gas, gasPrice, password: string, success: var bool): string =
let
label = fromHex(FixedBytes[32], label(username))
ensUsernamesContract = contracts.getContract("ens-usernames")
release = Release(label: label)
var tx = transactions.buildTokenTransaction(parseAddress(address), ensUsernamesContract.address, "", "")
try:
result = ensUsernamesContract.methods["release"].send(tx, release, password, success)
if success:
trackPendingTransaction(result, address, $ensUsernamesContract.address, PendingTransactionType.ReleaseENS, username)
except RpcException as e:
raise
proc getExpirationTime*(username: string, success: var bool): int =
let
label = fromHex(FixedBytes[32], label(username))
expTime = ExpirationTime(label: label)
ensUsernamesContract = contracts.getContract("ens-usernames")
var tx = transactions.buildTransaction(parseAddress("0x0000000000000000000000000000000000000000"), 0.u256)
tx.to = ensUsernamesContract.address.some
tx.data = ensUsernamesContract.methods["getExpirationTime"].encodeAbi(expTime)
var response = ""
try:
response = eth_transactions.call(tx).result
success = true
except RpcException as e:
success = false
error "Error obtaining expiration time", err=e.msg
if success:
result = fromHex[int](response)
proc extractCoordinates*(pubkey: string):tuple[x: string, y:string] =
result = ("0x" & pubkey[4..67], "0x" & pubkey[68..131])

View File

@ -29,6 +29,12 @@ type
x*: FixedBytes[32]
y*: FixedBytes[32]
ExpirationTime* = object
label*: FixedBytes[32]
Release* = object
label*: FixedBytes[32]
ApproveAndCall*[N: static[int]] = object
to*: Address
value*: Stuint[256]

View File

@ -10,7 +10,7 @@ import
export
GetPackData, PackData, BuyToken, ApproveAndCall, Transfer, BalanceOf, Register, SetPubkey,
TokenOfOwnerByIndex, TokenPackId, TokenUri, FixedBytes, DynamicBytes, toHex, fromHex,
decodeContractResponse, encodeAbi, estimateGas, send, call
decodeContractResponse, encodeAbi, estimateGas, send, call, ExpirationTime, Release
logScope:
@ -204,7 +204,9 @@ proc allContracts(): seq[Contract] =
Contract(name: "ens-usernames", network: Network.Mainnet, address: parseAddress("0xDB5ac1a559b02E12F29fC0eC0e37Be8E046DEF49"),
methods: [
("register", Method(signature: "register(bytes32,address,bytes32,bytes32)")),
("getPrice", Method(signature: "getPrice()"))
("getPrice", Method(signature: "getPrice()")),
("getExpirationTime", Method(signature: "getExpirationTime(bytes32)")),
("release", Method(signature: "release(bytes32)"))
].toTable
),
Contract(name: "ens-resolver", network: Network.Mainnet, address: parseAddress("0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41"),
@ -234,10 +236,12 @@ proc allContracts(): seq[Contract] =
),
newErc721Contract("sticker-pack", Network.Testnet, parseAddress("0xf852198d0385c4b871e0b91804ecd47c6ba97351"), "PACK", false, @[("tokenPackId", Method(signature: "tokenPackId(uint256)"))]),
newErc721Contract("kudos", Network.Testnet, parseAddress("0xcd520707fc68d153283d518b29ada466f9091ea8"), "KDO", true),
Contract(name: "ens-usernames", network: Network.Testnet, address: parseAddress("0x11d9F481effd20D76cEE832559bd9Aca25405841"),
Contract(name: "ens-usernames", network: Network.Testnet, address: parseAddress("0xdaae165beb8c06e0b7613168138ebba774aff071"),
methods: [
("register", Method(signature: "register(bytes32,address,bytes32,bytes32)")),
("getPrice", Method(signature: "getPrice()"))
("getPrice", Method(signature: "getPrice()")),
("getExpirationTime", Method(signature: "getExpirationTime(bytes32)")),
("release", Method(signature: "release(bytes32)"))
].toTable
),
Contract(name: "ens-resolver", network: Network.Testnet, address: parseAddress("0x42D63ae25990889E35F215bC95884039Ba354115"),

View File

@ -24,7 +24,7 @@ proc sendTransaction*(tx: EthSend, password: string): RpcResponse =
trace "Transaction sent succesfully", hash=result.result
proc call*(tx: EthSend): RpcResponse =
let responseStr = core.callPrivateRPC("eth_call", %*[%tx])
let responseStr = core.callPrivateRPC("eth_call", %*[%tx, "latest"])
result = Json.decode(responseStr, RpcResponse)
if not result.error.isNil:
raise newException(RpcException, "Error calling method: " & result.error.message)

View File

@ -10,8 +10,10 @@ Item {
property string username: ""
property string walletAddress: "-"
property string key: "-"
property var expiration: 0
signal backBtnClicked();
signal usernameReleased(username: string);
StyledText {
id: sectionTitle
@ -49,12 +51,17 @@ Item {
keyLbl.textToCopy = pubkey;
walletAddressLbl.visible = true;
keyLbl.visible = true;
releaseBtn.visible = isStatus
releaseBtn.enabled = (Date.now() / 1000) > expirationTime && expirationTime > 0 && profileModel.ens.preferredUsername != username
expiration = new Date(expirationTime * 1000).getTime()
}
onLoading: {
loadingImg.active = isLoading
if(!isLoading) return;
walletAddressLbl.visible = false;
keyLbl.visible = false;
releaseBtn.visible = false;
expiration = 0;
}
}
@ -84,6 +91,63 @@ Item {
anchors.topMargin: 24
}
Component {
id: transactionDialogComponent
StatusETHTransactionModal {
onOpened: {
walletModel.gasView.getGasPricePredictions()
}
title: qsTr("Connect username with your pubkey")
onClosed: {
destroy()
}
estimateGasFunction: function(selectedAccount) {
if (username === "" || !selectedAccount) return 100000;
return profileModel.ens.releaseEstimate(Utils.removeStatusEns(username), selectedAccount.address)
}
onSendTransaction: function(selectedAddress, gasLimit, gasPrice, password) {
return profileModel.ens.release(username,
selectedAddress,
gasLimit,
gasPrice,
password)
}
onSuccess: function(){
usernameReleased(username);
}
width: 475
height: 500
}
}
StatusButton {
id: releaseBtn
visible: false
enabled: false
anchors.top: keyLbl.bottom
anchors.topMargin: 24
anchors.left: parent.left
anchors.leftMargin: 24
text: qsTrId("Release username")
onClicked: {
openPopup(transactionDialogComponent)
}
}
Text {
visible: releaseBtn.visible && !releaseBtn.enabled
anchors.top: releaseBtn.bottom
anchors.topMargin: 2
anchors.left: parent.left
anchors.leftMargin: 24
text: profileModel.ens.preferredUsername != username ?
qsTr("Username locked. You wont be able to release it until %1").arg(Utils.formatShortDateStr(new Date(expiration).toDateString())):
qsTr("This is current preferred username. It can't be released")
color: Style.current.darkGrey
}
StatusButton {
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.current.padding

View File

@ -0,0 +1,95 @@
import QtQuick 2.14
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.14
import "../../../../../imports"
import "../../../../../shared"
import "../../../../../shared/status"
Item {
property string ensUsername: ""
signal okBtnClicked()
StyledText {
id: sectionTitle
//% "ENS usernames"
text: qsTrId("ens-usernames")
anchors.left: parent.left
anchors.leftMargin: Style.current.bigPadding
anchors.top: parent.top
anchors.topMargin: Style.current.bigPadding
font.weight: Font.Bold
font.pixelSize: 20
}
Rectangle {
id: circle
anchors.top: sectionTitle.bottom
anchors.topMargin: Style.current.bigPadding
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: title
text: qsTr("Username removed")
anchors.top: circle.bottom
anchors.topMargin: Style.current.bigPadding
font.weight: Font.Bold
font.pixelSize: 24
anchors.left: parent.left
anchors.right: parent.right
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
StyledText {
id: subtitle
text: qsTr("The username %1 will be removed and your deposit will be returned once the transaction is mined").arg(ensUsername)
anchors.top: title.bottom
anchors.topMargin: 24
font.pixelSize: 14
anchors.left: parent.left
anchors.right: parent.right
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
StyledText {
id: progress
//% "You can follow the progress in the Transaction History section of your wallet."
text: qsTrId("ens-username-you-can-follow-progress")
anchors.top: subtitle.bottom
anchors.topMargin: 24
font.pixelSize: 12
anchors.left: parent.left
anchors.right: parent.right
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
color: Style.current.secondaryText
}
StatusButton {
id: startBtn
anchors.top: progress.bottom
anchors.topMargin: Style.current.padding
anchors.horizontalCenter: parent.horizontalCenter
//% "Ok, got it"
text: qsTrId("ens-got-it")
onClicked: okBtnClicked()
}
}

View File

@ -47,25 +47,33 @@ Item {
Qt.callLater(validateENS, ensUsername, isStatus)
}
Loader {
id: transactionDialog
function open() {
this.active = true
this.item.open()
}
function closed() {
this.active = false // kill an opened instance
}
sourceComponent: SetPubKeyModal {
Component {
id: transactionDialogComponent
StatusETHTransactionModal {
onOpened: {
walletModel.gasView.getGasPricePredictions()
}
title: qsTr("Connect username with your pubkey")
onClosed: {
transactionDialog.closed()
destroy()
}
ensUsername: ensUsername.text || ""
width: 400
height: 400
estimateGasFunction: function(selectedAccount) {
if (ensUsername.text === "" || !selectedAccount) return 80000;
return profileModel.ens.setPubKeyGasEstimate(ensUsername.text + (isStatus ? ".stateofus.eth" : "" ), selectedAccount.address)
}
onSendTransaction: function(selectedAddress, gasLimit, gasPrice, password) {
return profileModel.ens.setPubKey(ensUsername.text + (isStatus ? ".stateofus.eth" : "" ),
selectedAddress,
gasLimit,
gasPrice,
password)
}
onSuccess: function(){
usernameUpdated(ensUsername.text);
}
width: 475
height: 500
}
}
@ -181,7 +189,7 @@ Item {
}
if(ensStatus === Constants.ens_connected_dkey || ensStatus === Constants.ens_owned){
transactionDialog.open();
openPopup(transactionDialogComponent)
return;
}
}

View File

@ -140,6 +140,10 @@ Item {
targetState: welcomeState
signal: goToWelcome
}
DSM.SignalTransition {
targetState: ensReleasedState
signal: done
}
}
DSM.State {
@ -176,6 +180,15 @@ Item {
}
}
DSM.State {
id: ensReleasedState
onEntered:loader.sourceComponent = ensReleased
DSM.SignalTransition {
targetState: listState
signal: next
}
}
DSM.State {
id: ensConnectedState
onEntered:loader.sourceComponent = ensConnected
@ -240,6 +253,14 @@ Item {
}
}
Component {
id: ensReleased
ENSReleased {
ensUsername: selectedUsername
onOkBtnClicked: next(null)
}
}
Component {
id: ensConnected
ENSConnected {
@ -265,6 +286,10 @@ Item {
ENSDetails {
username: selectedUsername
onBackBtnClicked: back();
onUsernameReleased: {
selectedUsername = username;
done(username);
}
}
}

View File

@ -2,17 +2,52 @@ import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtQuick.Dialogs 1.3
import "../../../../../imports"
import "../../../../../shared"
import "../../../../../shared/status"
import "../../imports"
import "../../shared"
import "../../shared/status"
ModalPopup {
id: root
readonly property var asset: {"name": "Ethereum", "symbol": "ETH"}
property string ensUsername: ""
//% "Connect username with your pubkey"
title: qsTrId("connect-username-with-your-pubkey")
title: qsTr("Contract interaction")
property var estimateGasFunction: (function(userAddress) { return 0; })
property var onSendTransaction: (function(userAddress, gasLimit, gasPrice, password){ return ""; })
property var onSuccess: (function(){})
Component.onCompleted: {
walletModel.gasView.getGasPricePredictions()
}
function sendTransaction() {
try {
let responseStr = onSendTransaction(selectFromAccount.selectedAccount.address,
gasSelector.selectedGasLimit,
gasSelector.selectedGasPrice,
transactionSigner.enteredPassword);
let response = JSON.parse(responseStr)
if (!response.success) {
if (Utils.isInvalidPasswordMessage(response.result)){
//% "Wrong password"
transactionSigner.validationError = qsTrId("wrong-password")
return
}
sendingError.text = response.result
return sendingError.open()
}
onSuccess();
root.close();
} catch (e) {
console.error('Error sending the transaction', e)
sendingError.text = "Error sending the transaction: " + e.message;
return sendingError.open()
}
}
property MessageDialog sendingError: MessageDialog {
id: sendingError
@ -22,33 +57,6 @@ ModalPopup {
standardButtons: StandardButton.Ok
}
function sendTransaction() {
try {
let responseStr = profileModel.ens.setPubKey(root.ensUsername,
selectFromAccount.selectedAccount.address,
gasSelector.selectedGasLimit,
gasSelector.selectedGasPrice,
transactionSigner.enteredPassword)
let response = JSON.parse(responseStr)
if (!response.success) {
if (Utils.isInvalidPasswordMessage(response.error.message)){
//% "Wrong password"
transactionSigner.validationError = qsTrId("wrong-password")
return
}
sendingError.text = response.error.message
return sendingError.open()
}
usernameUpdated(root.ensUsername);
} catch (e) {
console.error('Error sending the transaction', e)
sendingError.text = "Error sending the transaction: " + e.message;
return sendingError.open()
}
}
TransactionStackView {
id: stack
height: parent.height
@ -61,8 +69,7 @@ ModalPopup {
}
TransactionFormGroup {
id: group1
//% "Connect username with your pubkey"
headerText: qsTrId("connect-username-with-your-pubkey")
headerText: root.title
//% "Continue"
footerText: qsTrId("continue")
@ -104,11 +111,9 @@ ModalPopup {
getFiatValue: walletModel.balanceView.getFiatValue
defaultCurrency: walletModel.balanceView.defaultCurrency
property var estimateGas: Backpressure.debounce(gasSelector, 600, function() {
if (!(root.ensUsername !== "" && selectFromAccount.selectedAccount)) {
selectedGasLimit = 80000;
return;
}
selectedGasLimit = profileModel.ens.setPubKeyGasEstimate(root.ensUsername, selectFromAccount.selectedAccount.address)
let estimatedGas = root.estimateGasFunction(selectFromAccount.selectedAccount);
gasSelector.selectedGasLimit = estimatedGas
return estimatedGas;
})
}
GasValidator {
@ -123,8 +128,7 @@ ModalPopup {
}
TransactionFormGroup {
id: group3
//% "Connect username with your pubkey"
headerText: qsTrId("connect-username-with-your-pubkey")
headerText: root.title
//% "Sign with password"
footerText: qsTrId("sign-with-password")
@ -148,8 +152,7 @@ ModalPopup {
}
TransactionFormGroup {
id: group4
//% "Connect username with your pubkey"
headerText: qsTrId("connect-username-with-your-pubkey")
headerText: root.title
//% "Sign with password"
footerText: qsTrId("sign-with-password")
@ -164,6 +167,23 @@ ModalPopup {
footer: Item {
width: parent.width
height: btnNext.height
StatusRoundButton {
id: btnBack
anchors.left: parent.left
icon.name: "arrow-right"
icon.width: 20
icon.height: 16
rotation: 180
visible: stack.currentGroup.showBackBtn
enabled: stack.currentGroup.isValid || stack.isLastGroup
onClicked: {
if (typeof stack.currentGroup.onBackClicked === "function") {
return stack.currentGroup.onBackClicked()
}
stack.back()
}
}
StatusButton {
id: btnNext

View File

@ -115,7 +115,9 @@ ModalPopup {
defaultCurrency: walletModel.balanceView.defaultCurrency
width: stack.width
property var estimateGas: Backpressure.debounce(gasSelector, 600, function() {
return root.estimateGasFunction(selectFromAccount.selectedAccount, uuid);
let estimatedGas = root.estimateGasFunction(selectFromAccount.selectedAccount, uuid);
gasSelector.selectedGasLimit = estimatedGas
return estimatedGas;
})
}
GasValidator {