feat(TransactionModal): introduce async validation for ENS names

This commit extends the AddressInput to perform ENS lookups when valid
ENS values are entered. The lookup happens asynchronously, so we show a loading
indicator as the request is happening.

Closes #790
This commit is contained in:
Pascal Precht 2020-09-09 13:04:01 +02:00 committed by Iuri Matias
parent d64446f868
commit 729a2781f0
5 changed files with 83 additions and 18 deletions

View File

@ -5,6 +5,7 @@ import ../../status/libstatus/wallet as status_wallet
import ../../status/libstatus/tokens
import ../../status/libstatus/types
import ../../status/libstatus/utils as status_utils
import ../../status/ens as status_ens
import views/[asset_list, account_list, account_item, token_list, transaction_list, collectibles_list]
QtObject:
@ -527,3 +528,11 @@ QtObject:
proc wei2Token*(self: WalletView, wei: string, decimals: int): string {.slot.} =
return status_utils.wei2Token(wei, decimals)
proc resolveENS*(self: WalletView, ens: string) {.slot.} =
spawnAndSend(self, "ensResolved") do:
status_ens.owner(ens)
proc ensWasResolved*(self: WalletView, resolvedPubKey: string) {.signal.}
proc ensResolved(self: WalletView, pubKey: string) {.slot.} =
self.ensWasResolved(pubKey)

View File

@ -236,11 +236,10 @@ ModalPopup {
id: btnNext
anchors.right: parent.right
label: qsTr("Next")
disabled: !stack.currentGroup.isValid
disabled: !stack.currentGroup.isValid || stack.currentGroup.isPending
onClicked: {
const isValid = stack.currentGroup.validate()
if (stack.currentGroup.validate()) {
const validity = stack.currentGroup.validate()
if (validity.isValid && !validity.isPending) {
if (stack.isLastGroup) {
return root.sendTransaction()
}

View File

@ -70,6 +70,13 @@ QtObject {
return /^0x[a-fA-F0-9]{40}$/.test(inputValue)
}
function isValidEns(inputValue) {
const isEmail = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(inputValue)
const isDomain = /(?:(?:(?<thld>[\w\-]*)(?:\.))?(?<sld>[\w\-]*))\.(?<tld>[\w\-]*)/.test(inputValue)
return isEmail || isDomain || inputValue.startsWith("@")
}
/**
* Removes trailing zeros from a string-representation of a number. Throws
* if parameter is not a string

View File

@ -3,13 +3,16 @@ import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.13
import "../imports"
import "../shared"
Item {
id: root
property string validationError: "Error"
property string ensAsyncValidationError: qsTr("ENS Username not found")
property alias label: inpAddress.label
property string selectedAddress
property var isValid: false
property bool isPending: false
height: inpAddress.height
@ -20,24 +23,37 @@ Item {
isValid = false
}
function isValidEns(inputValue) {
// TODO: Check if the entered value resolves to an address. Long operation.
// Issue tracked: https://github.com/status-im/nim-status-client/issues/718
const isEmail = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(inputValue)
const isDomain = /(?:(?:(?<thld>[\w\-]*)(?:\.))?(?<sld>[\w\-]*))\.(?<tld>[\w\-]*)/.test(inputValue)
return isEmail || isDomain
}
function validate(inputValue) {
if (!inputValue) inputValue = selectedAddress
let isValid =
(inputValue && inputValue.startsWith("0x") && Utils.isValidAddress(inputValue)) ||
isValidEns(inputValue)
(inputValue && inputValue.startsWith("0x") && Utils.isValidAddress(inputValue) || Utils.isValidEns(inputValue))
inpAddress.validationError = isValid ? "" : validationError
root.isValid = isValid
return isValid
}
property var validateAsync: Backpressure.debounce(inpAddress, 300, function (inputValue) {
root.isPending = true
var name = inputValue.startsWith("@") ? inputValue.substring(1) : inputValue
walletModel.resolveENS(name)
});
Connections {
target: walletModel
onEnsWasResolved: {
root.isPending = false
if (resolvedPubKey === ""){
inpAddress.validationError = root.ensAsyncValidationError
root.isValid = false
} else {
root.isValid = true
root.selectedAddress = resolvedPubKey
inpAddress.validationError = ""
}
}
}
Input {
id: inpAddress
//% "eg. 0x1234 or ENS"
@ -64,7 +80,11 @@ Item {
metrics.text = text
const isValid = root.validate(inputValue)
if (isValid) {
if (Utils.isValidAddress(inputValue)) {
root.selectedAddress = inputValue
} else {
Qt.callLater(root.validateAsync, inputValue)
}
}
}
TextMetrics {
@ -86,6 +106,22 @@ Item {
}
}
}
Loader {
sourceComponent: loadingIndicator
anchors.top: inpAddress.bottom
anchors.right: inpAddress.right
anchors.topMargin: Style.current.halfPadding
active: root.isPending
}
Component {
id: loadingIndicator
LoadingImage {
width: 12
height: 12
}
}
}

View File

@ -5,6 +5,7 @@ import "../imports"
Rectangle {
id: root
property var isValid: true
property var isPending: false
property var validate: function() {
let isValid = true
for (let i=0; i<children.length; i++) {
@ -12,9 +13,13 @@ Rectangle {
if (component.hasOwnProperty("validate") && typeof component.validate === "function") {
isValid = component.validate()
}
if (component.hasOwnProperty("isPending")) {
isPending = component.isPending
}
}
root.isValid = isValid
return isValid
root.isPending = isPending
return { isValid, isPending }
}
color: Style.current.background
function reset() {
@ -41,19 +46,28 @@ Rectangle {
for (let i=0; i<children.length; i++) {
const component = children[i]
if (component.hasOwnProperty("isValid")) {
component.isValidChanged.connect(updateGroupValidity)
component.isValidChanged.connect(updateGroupValidityAndPendingStatus)
root.isValid = root.isValid && component.isValid // set the initial state
}
if (component.hasOwnProperty("isPending")) {
component.isPendingChanged.connect(updateGroupValidityAndPendingStatus)
root.isPending = component.isPending
}
}
}
function updateGroupValidity() {
function updateGroupValidityAndPendingStatus() {
let isValid = true
let isPending = false
for (let i=0; i<children.length; i++) {
const component = children[i]
if (component.hasOwnProperty("isValid")) {
isValid = isValid && component.isValid
}
if (component.hasOwnProperty("isPending")) {
isPending = component.isPending
}
}
root.isValid = isValid
root.isPending = isPending
}
}