feat(@desktop/wallet): update saved addresses list

Fixes #8599
This commit is contained in:
Ivan Belyakov 2023-02-20 13:57:45 +03:00 committed by IvanBelyakoff
parent 3897b42828
commit e27f2ec667
24 changed files with 942 additions and 127 deletions

View File

@ -204,7 +204,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
# result.mnemonicService = mnemonic_service.newService()
result.privacyService = privacy_service.newService(statusFoundation.events, result.settingsService,
result.accountsService)
result.savedAddressService = saved_address_service.newService(statusFoundation.events, result.networkService)
result.savedAddressService = saved_address_service.newService(statusFoundation.events, result.networkService, result.settingsService)
result.devicesService = devices_service.newService(statusFoundation.events, statusFoundation.threadpool, result.settingsService)
result.mailserversService = mailservers_service.newService(statusFoundation.events, statusFoundation.threadpool,
result.settingsService, result.nodeConfigurationService, statusFoundation.fleetConfiguration)
@ -541,4 +541,4 @@ proc addToKeycardUidPairsToCheckForAChangeAfterLogin*(self: AppController, oldKe
self.changedKeycardUids.add((oldKcUid: oldKeycardUid, newKcUid: newKeycardUid))
proc removeAllKeycardUidPairsForCheckingForAChangeAfterLogin*(self: AppController) =
self.changedKeycardUids = @[]
self.changedKeycardUids = @[]

View File

@ -28,8 +28,8 @@ proc init*(self: Controller) =
proc getSavedAddresses*(self: Controller): seq[saved_address_service.SavedAddressDto] =
return self.savedAddressService.getSavedAddresses()
proc createOrUpdateSavedAddress*(self: Controller, name: string, address: string, favourite: bool): string =
return self.savedAddressService.createOrUpdateSavedAddress(name, address, favourite)
proc createOrUpdateSavedAddress*(self: Controller, name: string, address: string, favourite: bool, chainShortNames: string, ens: string): string =
return self.savedAddressService.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
proc deleteSavedAddress*(self: Controller, address: string): string =
return self.savedAddressService.deleteSavedAddress(address)
proc deleteSavedAddress*(self: Controller, address: string, ens: string): string =
return self.savedAddressService.deleteSavedAddress(address, ens)

View File

@ -17,10 +17,10 @@ method viewDidLoad*(self: AccessInterface) {.base.} =
method loadSavedAddresses*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method createOrUpdateSavedAddress*(self: AccessInterface, name: string, address: string, favourite: bool): string {.base.} =
method createOrUpdateSavedAddress*(self: AccessInterface, name: string, address: string, favourite: bool, chainShortNames: string, ens: string): string {.base.} =
raise newException(ValueError, "No implementation available")
method deleteSavedAddress*(self: AccessInterface, address: string): string {.base.} =
method deleteSavedAddress*(self: AccessInterface, address: string, ens: string): string {.base.} =
raise newException(ValueError, "No implementation available")
type

View File

@ -6,17 +6,23 @@ type
address: string
ens: string
favourite: bool
chainShortNames: string
isTest: bool
proc initItem*(
name: string,
address: string,
favourite: bool,
ens: string
ens: string,
chainShortNames: string,
isTest: bool
): Item =
result.name = name
result.address = address
result.favourite = favourite
result.ens = ens
result.chainShortNames = chainShortNames
result.isTest = isTest
proc `$`*(self: Item): string =
result = fmt"""AllTokensItem(
@ -24,6 +30,8 @@ proc `$`*(self: Item): string =
address: {self.address},
favourite: {self.favourite},
ens: {self.ens},
chainShortNames: {self.chainShortNames},
isTest: {self.isTest},
]"""
proc getName*(self: Item): string =
@ -37,3 +45,9 @@ proc getAddress*(self: Item): string =
proc getFavourite*(self: Item): bool =
return self.favourite
proc getChainShortNames*(self: Item): string =
return self.chainShortNames
proc getIsTest*(self: Item): bool =
return self.isTest

View File

@ -8,6 +8,8 @@ type
Address
Favourite
Ens
ChainShortNames
IsTest
QtObject:
type
@ -47,6 +49,8 @@ QtObject:
ModelRole.Address.int:"address",
ModelRole.Favourite.int:"favourite",
ModelRole.Ens.int:"ens",
ModelRole.ChainShortNames.int:"chainShortNames",
ModelRole.IsTest.int:"isTest",
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
@ -68,6 +72,10 @@ QtObject:
result = newQVariant(item.getFavourite())
of ModelRole.Ens:
result = newQVariant(item.getEns())
of ModelRole.ChainShortNames:
result = newQVariant(item.getChainShortNames())
of ModelRole.IsTest:
result = newQVariant(item.getIsTest())
proc rowData(self: Model, index: int, column: string): string {.slot.} =
if (index >= self.items.len):
@ -78,6 +86,8 @@ QtObject:
of "address": result = $item.getAddress()
of "favourite": result = $item.getFavourite()
of "ens": result = $item.getEns()
of "chainShortNames": result = $item.getChainShortNames()
of "isTest": result = $item.getIsTest()
proc setItems*(self: Model, items: seq[Item]) =
self.beginResetModel()
@ -90,3 +100,15 @@ QtObject:
if(item.getAddress() == address):
return item.getName()
return ""
proc getChainShortNamesForAddress*(self: Model, address: string): string =
for item in self.items:
if(item.getAddress() == address):
return item.getChainShortNames()
return ""
proc getEnsForAddress*(self: Model, address: string): string =
for item in self.items:
if(item.getAddress() == address):
return item.getEns()
return ""

View File

@ -38,6 +38,8 @@ method loadSavedAddresses*(self: Module) =
s.address,
s.favourite,
s.ens,
s.chainShortNames,
s.isTest,
))
)
@ -55,8 +57,8 @@ method viewDidLoad*(self: Module) =
self.moduleLoaded = true
self.delegate.savedAddressesModuleDidLoad()
method createOrUpdateSavedAddress*(self: Module, name: string, address: string, favourite: bool): string =
return self.controller.createOrUpdateSavedAddress(name, address, favourite)
method createOrUpdateSavedAddress*(self: Module, name: string, address: string, favourite: bool, chainShortNames: string, ens: string): string =
return self.controller.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
method deleteSavedAddress*(self: Module, address: string): string =
return self.controller.deleteSavedAddress(address)
method deleteSavedAddress*(self: Module, address: string, ens: string): string =
return self.controller.deleteSavedAddress(address, ens)

View File

@ -37,11 +37,17 @@ QtObject:
proc setItems*(self: View, items: seq[Item]) =
self.model.setItems(items)
proc createOrUpdateSavedAddress*(self: View, name: string, address: string, favourite: bool): string {.slot.} =
return self.delegate.createOrUpdateSavedAddress(name, address, favourite)
proc createOrUpdateSavedAddress*(self: View, name: string, address: string, favourite: bool, chainShortNames: string, ens: string): string {.slot.} =
return self.delegate.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
proc deleteSavedAddress*(self: View, address: string): string {.slot.} =
return self.delegate.deleteSavedAddress(address)
proc deleteSavedAddress*(self: View, address: string, ens: string): string {.slot.} =
return self.delegate.deleteSavedAddress(address, ens)
proc getNameByAddress*(self: View, address: string): string {.slot.} =
return self.model.getNameByAddress(address)
proc getChainShortNamesForAddress*(self: View, address: string): string {.slot.} =
return self.model.getChainShortNamesForAddress(address)
proc getEnsForAddress*(self: View, address: string): string {.slot.} =
return self.model.getEnsForAddress(address)

View File

@ -8,20 +8,31 @@ type
address*: string
ens*: string
favourite*: bool
chainShortNames*: string
isTest*: bool
proc newSavedAddressDto*(
name: string,
address: string,
favourite: bool
ens: string,
favourite: bool,
chainShortNames: string,
isTest: bool
): SavedAddressDto =
return SavedAddressDto(
name: name,
address: address,
favourite: favourite
ens: ens,
favourite: favourite,
chainShortNames: chainShortNames,
isTest: isTest
)
proc toSavedAddressDto*(jsonObj: JsonNode): SavedAddressDto =
result = SavedAddressDto()
discard jsonObj.getProp("name", result.name)
discard jsonObj.getProp("address", result.address)
discard jsonObj.getProp("ens", result.ens)
discard jsonObj.getProp("favourite", result.favourite)
discard jsonObj.getProp("chainShortNames", result.chainShortNames)
discard jsonObj.getProp("isTest", result.isTest)

View File

@ -6,6 +6,7 @@ import ../../../app/core/eventemitter
import ../../../backend/backend
import ../../../app/core/[main]
import ../network/service as network_service
import ../settings/service as settings_service
export dto
@ -20,14 +21,17 @@ type
events: EventEmitter
savedAddresses: seq[SavedAddressDto]
networkService: network_service.Service
settingsService: settings_service.Service
proc delete*(self: Service) =
discard
proc newService*(events: EventEmitter, networkService: network_service.Service): Service =
proc newService*(events: EventEmitter, networkService: network_service.Service,
settingsService: settings_service.Service): Service =
result = Service()
result.events = events
result.networkService = networkService
result.settingsService = settingsService
proc fetchAddresses(self: Service) =
try:
@ -39,11 +43,12 @@ proc fetchAddresses(self: Service) =
)
let chainId = self.networkService.getNetworkForEns().chainId
for savedAddress in self.savedAddresses:
try:
let nameResponse = backend.getName(chainId, savedAddress.address)
savedAddress.ens = nameResponse.result.getStr
except:
continue
if savedAddress.ens != "":
try:
let nameResponse = backend.getName(chainId, savedAddress.address)
savedAddress.ens = nameResponse.result.getStr
except:
continue
except Exception as e:
error "error: ", procName="fetchAddress", errName = e.name, errDesription = e.msg
@ -64,9 +69,10 @@ proc init*(self: Service) =
proc getSavedAddresses*(self: Service): seq[SavedAddressDto] =
return self.savedAddresses
proc createOrUpdateSavedAddress*(self: Service, name: string, address: string, favourite: bool): string =
proc createOrUpdateSavedAddress*(self: Service, name: string, address: string, favourite: bool, chainShortNames: string, ens: string): string =
try:
discard backend.upsertSavedAddress(backend.SavedAddress(name: name, address: address, favourite: favourite))
let isTestAddress = self.settingsService.areTestNetworksEnabled()
discard backend.upsertSavedAddress(backend.SavedAddress(name: name, address: address, favourite: favourite, chainShortNames: chainShortNames, ens: ens, isTest: isTestAddress))
self.updateAddresses()
return ""
except Exception as e:
@ -74,9 +80,10 @@ proc createOrUpdateSavedAddress*(self: Service, name: string, address: string, f
error "error: ", errDesription
return errDesription
proc deleteSavedAddress*(self: Service, address: string): string =
proc deleteSavedAddress*(self: Service, address: string, ens: string): string =
try:
var response = backend.deleteSavedAddress(0, address)
let isTestAddress = self.settingsService.areTestNetworksEnabled()
var response = backend.deleteSavedAddress(address, ens, isTestAddress)
if not response.error.isNil:
raise newException(Exception, response.error.message)

View File

@ -29,6 +29,9 @@ type
name* {.serializedFieldName("name").}: string
address* {.serializedFieldName("address").}: string
favourite* {.serializedFieldName("favourite").}: bool
chainShortNames* {.serializedFieldName("chainShortNames").}: string
ens* {.serializedFieldName("ens").}: string
isTest* {.serializedFieldName("isTest").}: bool
Network* = ref object of RootObj
chainId* {.serializedFieldName("chainId").}: int
@ -82,8 +85,9 @@ rpc(upsertSavedAddress, "wakuext"):
savedAddress: SavedAddress
rpc(deleteSavedAddress, "wakuext"):
chainId: int
address: string
ens: string
isTest: bool
rpc(getSavedAddresses, "wallet"):
discard

View File

@ -0,0 +1,36 @@
pragma Singleton
import QtQuick 2.14
import utils 1.0
import StatusQ.Core.Theme 0.1
import "stores" as WalletStores
QtObject {
function colorizedChainPrefix(prefix) {
if (!prefix)
return ""
const prefixes = prefix.split(":").filter(Boolean)
let prefixStr = ""
const lastPrefixEndsWithColumn = prefix.endsWith(":")
const defaultColor = Theme.palette.baseColor1
for (let i in prefixes) {
const pref = prefixes[i]
let col = WalletStores.RootStore.colorForChainShortName(pref)
if (!col)
col = defaultColor
prefixStr += Utils.richColorText(pref, col)
// Avoid adding ":" if it was not there for the last prefix,
// because when user manually edits the address, it breaks editing
if (!(i == (prefixes.length - 1) && !lastPrefixEndsWithColumn)) {
prefixStr += Utils.richColorText(":", Theme.palette.baseColor1)
}
}
return prefixStr
}
}

View File

@ -99,7 +99,7 @@ Item {
multiSelection: root.multiSelection
onToggleNetwork: {
store.toggleNetwork(chainId)
store.toggleNetwork(network.chainId)
}
onSingleNetworkSelected: {

View File

@ -6,13 +6,13 @@ import utils 1.0
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
import shared.controls 1.0
import "../popups"
import "../controls"
import ".."
StatusListItem {
id: root
@ -22,55 +22,64 @@ StatusListItem {
property string name
property string address
property string ens
property string chainShortNames
property bool favourite: false
property var saveAddress: function (name, address, favourite) {}
property var deleteSavedAddress: function (address) {}
property var saveAddress: function (name, address, favourite, chainShortNames, ens) {}
property var deleteSavedAddress: function (address, ens) {}
signal openSendModal()
signal openSendModal(string recipient)
implicitWidth: parent.width
implicitWidth: ListView.view.width
title: name
objectName: name
subTitle: (ens.length > 0 ? ens + " \u2022 " : "")
+ Utils.elideText(address, 6, 4)
color: "transparent"
subTitle: {
if (ens.length > 0)
return ens
else
return WalletUtils.colorizedChainPrefix(chainShortNames) + address
}
border.color: Theme.palette.baseColor5
titleTextIcon: root.favourite ? "star-icon" : ""
asset.name: root.favourite ? "star-icon" : "favourite"
asset.color: root.favourite ? Theme.palette.pinColor1 : (showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1) // star icon color default
asset.hoverColor: root.favourite ? "transparent": Theme.palette.directColor1 // star icon color on hover
asset.bgColor: statusListItemIcon.hovered ? Theme.palette.primaryColor3 : "transparent" // icon outer background color
asset.bgRadius: 8
statusListItemIcon.hoverEnabled: true
onIconClicked: {
root.saveAddress(root.name, root.address, !root.favourite, root.chainShortNames, root.ens)
}
statusListItemSubTitle.font.pixelSize: 13
statusListItemSubTitle.customColor: !enabled ? Theme.palette.baseColor1 : Theme.palette.directColor1
statusListItemComponentsSlot.spacing: 0
property bool showButtons: sensor.containsMouse
QtObject {
id: d
readonly property string visibleAddress: root.address == Constants.zeroAddress ? root.ens : root.address
}
components: [
StatusRoundButton {
icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1
type: StatusRoundButton.Type.Tertiary
type: StatusRoundButton.Type.Quinary
radius: 8
icon.name: "send"
onClicked: openSendModal()
},
CopyToClipBoardButton {
id: copyButton
type: StatusRoundButton.Type.Tertiary
icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1
textToCopy: root.address
onCopyClicked: root.store.copyToClipboard(textToCopy)
},
StatusRoundButton {
objectName: "savedAddressView_Delegate_favouriteButton"
icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1
type: StatusRoundButton.Type.Tertiary
icon.name: root.favourite ? "unfavourite" : "favourite"
onClicked: {
root.saveAddress(root.name, root.address, !root.favourite)
}
onClicked: openSendModal(d.visibleAddress)
},
StatusRoundButton {
objectName: "savedAddressView_Delegate_menuButton"
visible: !!root.name
icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1
type: StatusRoundButton.Type.Tertiary
type: StatusRoundButton.Type.Quinary
radius: 8
icon.name: "more"
onClicked: {
editDeleteMenu.openMenu(root.name, root.address, root.favourite);
editDeleteMenu.openMenu(root.name, root.address, root.favourite, root.chainShortNames, root.ens);
}
},
StatusRoundButton {
@ -82,7 +91,8 @@ StatusListItem {
Global.openPopup(addEditSavedAddress,
{
addAddress: true,
address: root.address
address: d.visibleAddress,
ens: root.ens
})
}
}
@ -93,16 +103,22 @@ StatusListItem {
property string contactName
property string contactAddress
property bool storeFavourite
function openMenu(name, address, favourite) {
property string contactChainShortNames
property string contactEns
function openMenu(name, address, favourite, chainShortNames, ens) {
contactName = name;
contactAddress = address;
storeFavourite = favourite;
contactChainShortNames = chainShortNames;
contactEns = ens;
popup();
}
onClosed: {
contactName = "";
contactAddress = "";
storeFavourite = false;
contactChainShortNames = ""
contactEns = ""
}
StatusAction {
text: qsTr("Edit")
@ -114,10 +130,32 @@ StatusListItem {
edit: true,
address: editDeleteMenu.contactAddress,
name: editDeleteMenu.contactName,
favourite: editDeleteMenu.storeFavourite
favourite: editDeleteMenu.storeFavourite,
chainShortNames: editDeleteMenu.contactChainShortNames,
ens: editDeleteMenu.contactEns
})
}
}
StatusAction {
text: qsTr("Copy")
objectName: "copySavedAddressAction"
assetSettings.name: "copy"
onTriggered: {
if (d.visibleAddress)
store.copyToClipboard(d.visibleAddress)
else
store.copyToClipboard(root.ens)
}
}
StatusMenuSeparator { }
StatusAction {
text: qsTr("View on Etherscan")
objectName: "viewOnEtherscanAction"
assetSettings.name: "external"
onTriggered: {
Global.openLink("https://etherscan.io/address/%1".arg(d.visibleAddress ? d.visibleAddress : root.ens))
}
}
StatusMenuSeparator { }
StatusAction {
text: qsTr("Delete")
@ -128,6 +166,7 @@ StatusListItem {
deleteAddressConfirm.name = editDeleteMenu.contactName;
deleteAddressConfirm.address = editDeleteMenu.contactAddress;
deleteAddressConfirm.favourite = editDeleteMenu.storeFavourite;
deleteAddressConfirm.ens = editDeleteMenu.contactEns
deleteAddressConfirm.open()
}
}
@ -140,8 +179,9 @@ StatusListItem {
anchors.centerIn: parent
onClosed: destroy()
contactsStore: root.contactsStore
store: root.store
onSave: {
root.saveAddress(name, address, favourite)
root.saveAddress(name, address, favourite, chainShortNames, ens)
close()
}
}
@ -150,6 +190,7 @@ StatusListItem {
StatusModal {
id: deleteAddressConfirm
property string address
property string ens
property string name
property bool favourite
anchors.centerIn: parent
@ -177,7 +218,7 @@ StatusListItem {
objectName: "confirmDeleteSavedAddress"
text: qsTr("Delete")
onClicked: {
root.deleteSavedAddress(deleteAddressConfirm.address)
root.deleteSavedAddress(deleteAddressConfirm.address, deleteAddressConfirm.ens)
deleteAddressConfirm.close()
}
}

View File

@ -0,0 +1,96 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
Control {
id: root
property alias titleText: titleText
property alias button: button
property string title: ""
signal clicked(var mouse)
property StatusAssetSettings asset: StatusAssetSettings {
height: 20
width: 20
rotation: 0
isLetterIdenticon: false
letterSize: 10
color: "transparent"
bgWidth: 15
bgHeight: 15
bgColor: "transparent"
bgBorderColor: Theme.palette.baseColor2
bgRadius: 16
imgIsIdenticon: false
}
QtObject {
id: d
readonly property int commonMargin: 5
readonly property int leftMargin: 8
readonly property int minHeight: 32
}
leftPadding: d.leftMargin
spacing: d.commonMargin
implicitHeight: d.minHeight
background: Rectangle {
color: root.hovered ? Theme.palette.primaryColor3 : asset.bgColor
radius: asset.bgRadius
border.color: asset.bgBorderColor
MouseArea {
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
onClicked: root.clicked(mouse)
}
}
contentItem: RowLayout {
spacing: root.spacing
StatusSmartIdenticon {
id: iconOrImage
asset: root.asset
name: root.title
active: root.asset.isLetterIdenticon ||
!!root.asset.name
}
StatusBaseText {
id: titleText
Layout.rightMargin: button.visible ? 0 : d.commonMargin
Layout.fillWidth: true
color: enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
text: root.title
font.pixelSize: 15
font.weight: Font.Medium
elide: Text.ElideRight
}
StatusRoundButton {
id: button
Layout.preferredHeight: root.height - d.commonMargin
Layout.preferredWidth: root.height - d.commonMargin
Layout.rightMargin: d.commonMargin
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
radius: height / 2
type: StatusRoundButton.Tertiary
icon.name: "close"
}
}
}

View File

@ -0,0 +1,218 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import QtQuick.Controls 2.14 as QC
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Utils 0.1
import utils 1.0
/*!
\qmltype StatusNetworkSelector
\inherits Rectangle
\inqmlmodule StatusQ.Components
\since StatusQ.Components 0.1
\brief It allows to add items and display them as a tag item with an image and text. It also allows to store and display logical `and` / `or` operators into the list. Inherits \l{https://doc.qt.io/qt-6/qml-qtquick-rectangle.html}{Item}.
The \c StatusNetworkSelector is populated with a data model. The data model is commonly a JavaScript array or a ListModel object with specific expected roles.
Example of how the component looks like:
\image status_item_selector.png
Example of how to use it:
\qml
StatusNetworkSelector {
id: networkSelector
title: "Network preference"
enabled: addressInput.valid
defaultItemText: "Add networks"
defaultItemImageSource: "add"
itemsModel: ListModel {}
addButton.onClicked: {
}
onItemClicked: {
}
onItemRightButtonClicked: {
}
}
\endqml
For a list of components available see StatusQ.
*/
Rectangle {
id: root
/*!
\qmlproperty string StatusNetworkSelector::title
This property holds the title shown on top of the component.
*/
property string title
/*!
\qmlproperty string StatusNetworkSelector::defaultItemText
This property holds the default item text shown when the list of items is empty.
*/
property string defaultItemText
/*!
\qmlproperty url StatusNetworkSelector::defaultItemImageSource
This property holds the default item icon shown when the list of items is empty.
*/
property string defaultItemImageSource: ""
/*!
\qmlproperty StatusRoundButton StatusNetworkSelector::addButton
This property holds an alias to the `add` button.
*/
readonly property alias addButton: addItemButton
/*!
\qmlproperty ListModel StatusNetworkSelector::itemsModel
This property holds the data that will be populated in the items selector.
Here an example of the model roles expected:
\qml
itemsModel: ListModel {
ListElement {
text: "Ethereum"
iconUrl: "Network=Ethereum"
}
ListElement {
text: "Optimism"
iconUrl: "Network=Optimism"
}
}
\endqml
*/
property var itemsModel: ListModel { }
/*!
\qmlproperty bool StatusNetworkSelector::useIcons
This property determines if the imageSource role from the model will be handled as
an image or an icon.
*/
property bool useIcons: false
property StatusAssetSettings asset: StatusAssetSettings {
height: 20
width: 20
bgColor: "transparent"
isImage: !root.useIcons
isLetterIdenticon: root.useLetterIdenticons
}
/*!
\qmlproperty bool StatusNetworkSelector::useLetterIdenticons
This property determines if letter identicons should be used. If set to
true, the model is expected to contain roles "color" and "emoji".
*/
property bool useLetterIdenticons: false
/*!
\qmlsignal StatusNetworkSelector::itemClicked
This signal is emitted when the item is clicked.
*/
signal itemClicked(var item, int index, var mouse)
/*!
\qmlsignal StatusNetworkSelector::itemRightButtonClicked
This signal is emitted when the item's right button is clicked.
*/
signal itemRightButtonClicked(var item, int index, var mouse)
color: "transparent"
implicitHeight: columnLayout.implicitHeight
implicitWidth: 560
property bool rightButtonVisible: false
/*!
\qmlproperty StatusNetworkListItemTag StatusNetworkSelector::defaultItem
This property holds an alias to the `defaultItem` tag
*/
property alias defaultItem: defaultListItemTag
ColumnLayout {
id: columnLayout
spacing: 8
StatusBaseText {
text: root.title
color: Theme.palette.directColor1
font.pixelSize: 15
}
Flow {
id: flow
Layout.preferredWidth: root.width
Layout.fillWidth: true
spacing: 6
StatusRoundButton {
id: addItemButton
implicitHeight: 32
implicitWidth: implicitHeight
height: width
type: StatusRoundButton.Type.Tertiary
border.color: Theme.palette.baseColor2
icon.name: root.defaultItemImageSource
visible: itemsModel.count > 0
icon.color: Theme.palette.primaryColor1
}
StatusNetworkListItemTag {
id: defaultListItemTag
visible: !itemsModel || itemsModel.count === 0
title: root.defaultItemText
button.visible: true
button.icon.name: root.defaultItemImageSource
button.enabled: false
button.icon.disabledColor: titleText.color
button.icon.color: titleText.color
onClicked: {
root.itemClicked(this, 0, mouse)
}
}
Repeater {
model: itemsModel
StatusNetworkListItemTag {
id: networkTag
title: model.chainName
asset.height: root.asset.height
asset.width: root.asset.width
asset.name: root.useLetterIdenticons ? model.text : Style.svg(model.iconUrl)
asset.isImage: root.asset.isImage
asset.bgColor: root.asset.bgColor
asset.isLetterIdenticon: root.useLetterIdenticons
button.visible: root.rightButtonVisible
titleText.color: Theme.palette.primaryColor1
button.icon.disabledColor: titleText.color
button.icon.color: titleText.color
hoverEnabled: false
property var modelRef: model // model is not reachable outside via item.model.someData, so expose it
onClicked: {
root.itemClicked(this, index, mouse)
}
button.onClicked: {
root.itemRightButtonClicked(networkTag, index, mouse)
}
}
}
}
}
}

View File

@ -1,6 +1,7 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQml.Models 2.14
import QtQuick.Layouts 1.14
import utils 1.0
import shared.controls 1.0
@ -11,28 +12,53 @@ import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Components 0.1
import SortFilterProxyModel 0.2
import "../controls"
import "../stores"
import ".."
StatusDialog {
id: root
property bool edit: false
property bool addAddress: false
property string address
property string address: Constants.zeroAddress // Setting as zero address since we don't have the address yet
property string chainShortNames
property string ens
property alias name: nameInput.text
property bool favourite: false
property var contactsStore
property var store
signal save(string name, string address)
signal save(string name, string address, string chainShortNames, string ens)
QtObject {
id: d
property int validationMode: root.edit ?
readonly property int validationMode: root.edit ?
StatusInput.ValidationMode.Always
: StatusInput.ValidationMode.OnlyWhenDirty
property bool valid: addressInput.isValid && nameInput.valid // TODO: Add network preference and emoji
property bool dirty: nameInput.input.dirty
readonly property bool valid: addressInput.valid && nameInput.valid
property bool chainShortNamesDirty: false
readonly property bool dirty: nameInput.input.dirty || chainShortNamesDirty
readonly property var chainPrefixRegexPattern: /[^:]+\:?|:/g
readonly property string visibleAddress: root.address == Constants.zeroAddress ? "" : root.address
readonly property bool addressInputIsENS: !visibleAddress
function getPrefixArrayWithColumns(prefixStr) {
return prefixStr.match(d.chainPrefixRegexPattern)
}
function resetAddressValues() {
root.ens = ""
root.address = Constants.zeroAddress
root.chainShortNames = ""
allNetworksModelCopy.setEnabledNetworks([])
}
}
width: 574
@ -46,7 +72,10 @@ StatusDialog {
onOpened: {
if(edit || addAddress) {
addressInput.input.text = root.address
if (root.ens)
addressInput.setPlainText(root.ens)
else
addressInput.setPlainText(root.chainShortNames + d.visibleAddress)
}
nameInput.input.edit.forceActiveFocus(Qt.MouseFocusReason)
}
@ -54,7 +83,7 @@ StatusDialog {
Column {
width: parent.width
height: childrenRect.height
topPadding: Style.current.xlPadding
topPadding: Style.current.bigPadding
spacing: Style.current.bigPadding
@ -62,9 +91,7 @@ StatusDialog {
id: nameInput
implicitWidth: parent.width
input.edit.objectName: "savedAddressNameInput"
minimumHeight: 56
maximumHeight: 56
placeholderText: qsTr("Enter a name")
placeholderText: qsTr("Address owner")
label: qsTr("Name")
validators: [
StatusMinLengthValidator {
@ -76,33 +103,226 @@ StatusDialog {
errorMessage: qsTr("This is not a valid account name")
}
]
input.clearable: true
input.rightPadding: 16
charLimit: 40
validationMode: d.validationMode
}
// To-Do use StatusInput within the below component
RecipientSelector {
StatusInput {
id: addressInput
implicitWidth: parent.width
inputWidth: implicitWidth
accounts: RootStore.accounts
contactsStore: root.contactsStore
label: qsTr("Address")
input.textField.objectName: "savedAddressAddressInput"
input.placeholderText: qsTr("Enter ENS Name or Ethereum Address")
labelFont.pixelSize: 15
labelFont.weight: Font.Normal
input.implicitHeight: 56
input.textField.anchors.rightMargin: 0
isSelectorVisible: false
addContactEnabled: false
onSelectedRecipientChanged: {
root.address = selectedRecipient.address
input.edit.objectName: "savedAddressAddressInput"
placeholderText: qsTr("Ethereum Address")
maximumHeight: 66
input.implicitHeight: Math.min(Math.max(input.edit.contentHeight + topPadding + bottomPadding, minimumHeight), maximumHeight) // setting height instead does not work
enabled: !(root.edit || root.addAddress)
validators: [
StatusMinLengthValidator {
minLength: 1
errorMessage: qsTr("Address must not be blank")
},
StatusValidator {
errorMessage: addressInput.plainText ? qsTr("Please enter a valid address or ENS name.") : ""
validate: function (t) {
return Utils.isValidAddressWithChainPrefix(t) || Utils.isValidEns(t)
? true : { actual: t }
}
}
]
validationMode: d.validationMode
input.edit.textFormat: TextEdit.RichText
input.asset.name: addressInput.valid && !root.edit ? "checkbox" : ""
input.asset.color: enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
input.rightPadding: 16
input.leftIcon: false
multiline: true
property string plainText: input.edit.getText(0, text.length)
onTextChanged: {
if (skipTextUpdate)
return
plainText = input.edit.getText(0, text.length)
if (input.edit.previousText != plainText) {
let newText = plainText
const prefixAndAddress = Utils.splitToChainPrefixAndAddress(plainText)
if (!Utils.isLikelyEnsName(plainText)) {
newText = WalletUtils.colorizedChainPrefix(prefixAndAddress.prefix) +
prefixAndAddress.address
}
setRichText(newText)
// Reset
if (plainText.length == 0) {
d.resetAddressValues()
return
}
// Update root values
if (Utils.isLikelyEnsName(plainText)) {
root.ens = plainText
root.address = Constants.zeroAddress
root.chainShortNames = ""
}
else {
root.ens = ""
root.address = prefixAndAddress.address
root.chainShortNames = prefixAndAddress.prefix
let prefixArrWithColumn = d.getPrefixArrayWithColumns(prefixAndAddress.prefix)
if (!prefixArrWithColumn)
prefixArrWithColumn = []
allNetworksModelCopy.setEnabledNetworks(prefixArrWithColumn)
}
}
}
property bool skipTextUpdate: false
function setPlainText(newText) {
text = newText
}
function setRichText(val) {
skipTextUpdate = true
input.edit.previousText = plainText
const curPos = input.cursorPosition
setPlainText(val)
input.cursorPosition = curPos
skipTextUpdate = false
}
function getUnknownPrefixes(prefixes) {
let unknownPrefixes = prefixes.filter(e => {
for (let i = 0; i < allNetworksModelCopy.count; i++) {
if (e == allNetworksModelCopy.get(i).shortName)
return false
}
return true
})
return unknownPrefixes
}
// Add all chain short names from model, while keeping existing
function syncChainPrefixWithModel(prefix, model) {
let prefixes = prefix.split(":").filter(Boolean)
let prefixStr = ""
// Keep unknown prefixes from user input, the rest must be taken
// from the model
for (let i = 0; i < model.count; i++) {
const item = model.get(i)
prefixStr += item.shortName + ":"
// Remove all added prefixes from initial array
prefixes = prefixes.filter(e => e !== item.shortName)
}
const unknownPrefixes = getUnknownPrefixes(prefixes)
if (unknownPrefixes.length > 0) {
prefixStr += unknownPrefixes.join(":") + ":"
}
return prefixStr
}
readOnly: root.edit || root.addAddress
wrongInputValidationError: qsTr("Please enter a valid ENS name OR Ethereum Address")
ownAddressError: qsTr("Can't add yourself as a saved address")
}
StatusNetworkSelector {
id: networkSelector
title: "Network preference"
enabled: addressInput.valid && !d.addressInputIsENS
defaultItemText: "Add networks"
defaultItemImageSource: "add"
rightButtonVisible: true
property bool modelUpdateBlocked: false
function blockModelUpdate(value) {
modelUpdateBlocked = value
}
itemsModel: SortFilterProxyModel {
sourceModel: allNetworksModelCopy
filters: ValueFilter {
roleName: "isEnabled"
value: true
}
onCountChanged: {
if (!networkSelector.modelUpdateBlocked) {
// Initially source model is empty, filter proxy is also empty, but does
// extra work and mistakenly overwrites root.chainShortNames property
if (sourceModel.count != 0) {
const prefixAndAddress = Utils.splitToChainPrefixAndAddress(addressInput.plainText)
const syncedPrefix = addressInput.syncChainPrefixWithModel(prefixAndAddress.prefix, this)
root.chainShortNames = syncedPrefix
addressInput.setPlainText(syncedPrefix + prefixAndAddress.address)
}
}
}
}
addButton.highlighted: networkSelectPopup.visible
addButton.onClicked: {
networkSelectPopup.openAtPosition(addButton.x, networkSelector.y + addButton.height + Style.current.xlPadding)
}
onItemClicked: function (item, index, mouse) {
// Append first item
if (index === 0 && defaultItem.visible)
networkSelectPopup.openAtPosition(defaultItem.x, networkSelector.y + defaultItem.height + Style.current.xlPadding)
}
onItemRightButtonClicked: function (item, index, mouse) {
item.modelRef.isEnabled = !item.modelRef.isEnabled
d.chainShortNamesDirty = true
}
}
}
NetworkSelectPopup {
id: networkSelectPopup
layer1Networks: SortFilterProxyModel {
sourceModel: allNetworksModelCopy
filters: ValueFilter {
roleName: "layer"
value: 1
}
}
layer2Networks: SortFilterProxyModel {
sourceModel: allNetworksModelCopy
filters: ValueFilter {
roleName: "layer"
value: 2
}
}
onToggleNetwork: {
network.isEnabled = !network.isEnabled
d.chainShortNamesDirty = true
}
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
function openAtPosition(xPos, yPos) {
x = xPos
y = yPos
open()
}
modal: true
dim: false
}
footer: StatusDialogFooter {
@ -110,9 +330,43 @@ StatusDialog {
StatusButton {
text: root.edit ? qsTr("Save") : qsTr("Add address")
enabled: d.valid && d.dirty
onClicked: root.save(name, address)
onClicked: root.save(name, address, chainShortNames, ens)
objectName: "addSavedAddress"
}
}
}
ListModel {
id: allNetworksModelCopy
function setEnabledNetworks(prefixArr) {
networkSelector.blockModelUpdate(true)
for (let i = 0; i < count; i++) {
// Add only those chainShortNames to the model, that have column ":" at the end, making it a valid chain prefix
setProperty(i, "isEnabled", prefixArr.includes(get(i).shortName + ":"))
}
networkSelector.blockModelUpdate(false)
}
function init(model, address) {
const prefixStr = Utils.getChainsPrefix(address)
for (let i = 0; i < model.count; i++) {
const clonedItem = {
layer: model.rowData(i, "layer"),
chainId: model.rowData(i, "chainId"),
chainColor: model.rowData(i, "chainColor"),
chainName: model.rowData(i, "chainName"),
shortName: model.rowData(i, "shortName"),
iconUrl: model.rowData(i, "iconUrl"),
isEnabled: Boolean(prefixStr.length > 0 && prefixStr.includes(shortName))
}
append(clonedItem)
}
}
}
Component.onCompleted: {
allNetworksModelCopy.init(store.allNetworks, root.address)
}
}

View File

@ -30,7 +30,7 @@ Popup {
property bool multiSelection: true
signal toggleNetwork(int chainId)
signal toggleNetwork(var network)
signal singleNetworkSelected(int chainId, string chainName, string iconUrl)
background: Rectangle {
@ -114,7 +114,7 @@ Popup {
asset.name: Style.svg(model.iconUrl)
onClicked: {
if(root.multiSelection)
checkBox.toggle()
checkBox.toggled()
else
radioButton.toggle()
}
@ -124,10 +124,10 @@ Popup {
visible: root.multiSelection
checked: root.useNetworksExtraStoreProxy ? model.isActive : model.isEnabled
onToggled: {
if(root.useNetworksExtraStoreProxy && model.isActive !== checked) {
model.isActive = checked
} else if (model.isEnabled !== checked) {
root.toggleNetwork(model.chainId)
if (root.useNetworksExtraStoreProxy) {
model.isActive = !model.isActive
} else {
root.toggleNetwork(model)
}
}
},

View File

@ -2,3 +2,4 @@ LeftTabView 1.0 LeftTabView.qml
WalletHeader 1.0 WalletHeader.qml
CollectiblesView 1.0 CollectiblesView.qml
WalletLayout 1.0 WalletLayout.qml
singleton WalletUtils 1.0 WalletUtils.qml

View File

@ -3,7 +3,9 @@ pragma Singleton
import QtQuick 2.13
import utils 1.0
import "../panels"
import SortFilterProxyModel 0.2
import StatusQ.Core.Theme 0.1
QtObject {
id: root
@ -30,7 +32,30 @@ QtObject {
property var flatCollectibles: walletSectionCollectibles.flatModel
property var currentCollectible: walletSectionCurrentCollectible
property var savedAddresses: walletSectionSavedAddresses.model
property var savedAddresses: SortFilterProxyModel {
sourceModel: walletSectionSavedAddresses.model
filters: [
ValueFilter {
roleName: "isTest"
value: networksModule.areTestNetworksEnabled
}
]
}
property QtObject _d: QtObject {
id: d
property var chainColors: ({})
function initChainColors(model) {
for (let i = 0; i < model.count; i++) {
chainColors[model.rowData(i, "shortName")] = model.rowData(i, "chainColor")
}
}
}
function colorForChainShortName(chainShortName) {
return d.chainColors[chainShortName]
}
// Used for new wallet account generation
property var generatedAccountsViewModel: walletSectionAccounts.generatedAccounts
@ -41,6 +66,9 @@ QtObject {
property var testNetworks: networksModule.test
property var enabledNetworks: networksModule.enabled
property var allNetworks: networksModule.all
onAllNetworksChanged: {
d.initChainColors(allNetworks)
}
property var layer1NetworksProxy: networksModule.layer1Proxy
property var layer2NetworksProxy: networksModule.layer2Proxy
@ -170,12 +198,12 @@ QtObject {
walletSectionCurrentCollectible.update(slug, id)
}
function createOrUpdateSavedAddress(name, address, favourite) {
return walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, favourite)
function createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens) {
return walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
}
function deleteSavedAddress(address) {
return walletSectionSavedAddresses.deleteSavedAddress(address)
function deleteSavedAddress(address, ens) {
return walletSectionSavedAddresses.deleteSavedAddress(address, ens)
}
function toggleNetwork(chainId) {

View File

@ -6,7 +6,6 @@ import utils 1.0
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
import shared.controls 1.0
@ -27,16 +26,21 @@ Item {
id: _internal
property bool loading: false
property string error: ""
function saveAddress(name, address, favourite) {
property var lastCreatedAddress // used to display animation for the newly saved address
function saveAddress(name, address, favourite, chainShortNames, ens) {
loading = true
error = RootStore.createOrUpdateSavedAddress(name, address, favourite)
error = RootStore.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
loading = false
}
function deleteSavedAddress(address) {
function deleteSavedAddress(address, ens) {
loading = true
error = RootStore.deleteSavedAddress(address)
error = RootStore.deleteSavedAddress(address, ens)
loading = false
}
function resetLastCreatedAddress() {
lastCreatedAddress = undefined
}
}
Item {
@ -105,21 +109,46 @@ Item {
spacing: 5
model: RootStore.savedAddresses
delegate: SavedAddressesDelegate {
id: savedAddressDelegate
objectName: "savedAddressView_Delegate_" + name
name: model.name
address: model.address
chainShortNames: model.chainShortNames
ens: model.ens
favourite: model.favourite
store: RootStore
contactsStore: root.contactsStore
onOpenSendModal: root.sendModal.open(address);
saveAddress: function(name, address, favourite) {
_internal.saveAddress(name, address, favourite)
onOpenSendModal: root.sendModal.open(recipient);
saveAddress: function(name, address, favourite, chainShortNames, ens) {
_internal.saveAddress(name, address, favourite, chainShortNames, ens)
}
deleteSavedAddress: function(address) {
_internal.deleteSavedAddress(address)
deleteSavedAddress: function(address, ens) {
_internal.deleteSavedAddress(address, ens)
}
states: [
State {
name: "highlighted"
when: _internal.lastCreatedAddress ? (_internal.lastCreatedAddress.address.toLowerCase() === address.toLowerCase() &&
_internal.lastCreatedAddress.ens === ens) : false
PropertyChanges { target: savedAddressDelegate; color: Theme.palette.baseColor2 }
StateChangeScript {
script: Qt.callLater(_internal.resetLastCreatedAddress)
}
}
]
transitions: [
Transition {
from: "highlighted"
ColorAnimation {
target: savedAddressDelegate
duration: 3000
}
}
]
}
}
@ -130,8 +159,10 @@ Item {
anchors.centerIn: parent
onClosed: destroy()
contactsStore: root.contactsStore
store: RootStore
onSave: {
_internal.saveAddress(name, address, favourite)
_internal.lastCreatedAddress = { address: address, ens: ens }
_internal.saveAddress(name, address, favourite, chainShortNames, ens)
close()
}
}

View File

@ -14,6 +14,8 @@ import utils 1.0
import shared.stores 1.0
import "../controls"
import "../stores" as WalletStores
import ".."
Item {
id: root
@ -31,6 +33,8 @@ Item {
readonly property string savedAddressNameFrom: root.isTransactionValid ? d.getNameForSavedWalletAddress(transaction.from): ""
readonly property string from: root.isTransactionValid ? !!savedAddressNameFrom ? savedAddressNameFrom : Utils.compactAddress(transaction.from, 4): ""
readonly property string to: root.isTransactionValid ? !!savedAddressNameTo ? savedAddressNameTo : Utils.compactAddress(transaction.to, 4): ""
readonly property string savedAddressEns: RootStore.getEnsForSavedWalletAddress(isIncoming ? transaction.from : transaction.to)
readonly property string savedAddressChains: RootStore.getChainShortNamesForSavedWalletAddress(isIncoming ? transaction.from : transaction.to)
function getNameForSavedWalletAddress(address) {
return RootStore.getNameForSavedWalletAddress(address)
@ -81,16 +85,18 @@ Item {
name: d.isIncoming ? d.savedAddressNameFrom : d.savedAddressNameTo
address: root.isTransactionValid ? d.isIncoming ? transaction.from : transaction.to : ""
ens: d.savedAddressEns
chainShortNames: d.savedAddressChains
title: d.isIncoming ? d.from : d.to
subTitle: root.isTransactionValid ? d.isIncoming ? !!d.savedAddressNameFrom ? Utils.compactAddress(transaction.from, 4) : "" : !!d.savedAddressNameTo ? Utils.compactAddress(transaction.to, 4) : "": ""
store: RootStore
store: WalletStores.RootStore
contactsStore: root.contactsStore
onOpenSendModal: root.sendModal.open(address);
saveAddress: function(name, address, favourite) {
RootStore.createOrUpdateSavedAddress(name, address, favourite)
saveAddress: function(name, address, favourite, chainShortNames, ens) {
RootStore.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
}
deleteSavedAddress: function(address) {
RootStore.deleteSavedAddress(address)
deleteSavedAddress: function(address, ens) {
RootStore.deleteSavedAddress(address, ens)
}
}

View File

@ -375,6 +375,7 @@ StatusDialog {
label: qsTr("To")
placeholderText: qsTr("Enter an ENS name or address")
text: popup.addressText
input.background.color: Theme.palette.indirectColor1
input.background.border.width: 0
input.implicitHeight: 56

View File

@ -193,12 +193,20 @@ QtObject {
return walletSectionSavedAddresses.getNameByAddress(address)
}
function createOrUpdateSavedAddress(name, address, favourite) {
return walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, favourite)
function getChainShortNamesForSavedWalletAddress(address) {
return walletSectionSavedAddresses.getChainShortNamesForAddress(address)
}
function deleteSavedAddress(address) {
return walletSectionSavedAddresses.deleteSavedAddress(address)
function getEnsForSavedWalletAddress(address) {
return walletSectionSavedAddresses.getEnsForAddress(address)
}
function createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens) {
return walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
}
function deleteSavedAddress(addresse, ens) {
return walletSectionSavedAddresses.deleteSavedAddress(address, ens)
}
function getLatestBlockNumber() {

View File

@ -38,6 +38,35 @@ QtObject {
return startsWith0x(value) && isHex(value) && value.length === 42
}
function isValidAddressWithChainPrefix(value) {
return value.match(/^(([a-zA-Z]{3,5}:)*)?(0x[a-fA-F0-9]{40})$/)
}
function getChainsPrefix(address) {
// matchAll is not supported by QML JS engine
return address.match(/([a-zA-Z]{3,5}:)*/)[0].split(':').filter(e => !!e)
}
function isLikelyEnsName(text) {
return text.startsWith("@") || !isLikelyAddress(text)
}
function isLikelyAddress(text) {
return text.includes(":") || text.includes('0x')
}
function richColorText(text, color) {
return "<font color=\"" + color + "\">" + text + "</font>"
}
function splitToChainPrefixAndAddress(input) {
const addressIdx = input.indexOf('0x')
if (addressIdx < 0)
return { prefix: input, address: "" }
return { prefix: input.substring(0, addressIdx), address: input.substring(addressIdx) }
}
function isPrivateKey(value) {
return isHex(value) && ((startsWith0x(value) && value.length === 66) ||
(!startsWith0x(value) && value.length === 64))
@ -109,7 +138,7 @@ QtObject {
return false
}
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)
const isDomain = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/.test(inputValue)
return isEmail || isDomain || (inputValue.startsWith("@") && inputValue.length > 1)
}