feat(dapps) check Wallet Connect pair uri for already used and expired

Refactor and bring all the error check back to services
Automate pairing procedure and move to the connect dialog
if uri was validated

Updates: #14676
This commit is contained in:
Stefan 2024-07-05 12:32:31 +03:00 committed by Stefan Dunca
parent 2df35ff0c9
commit 15b2c084d2
9 changed files with 144 additions and 44 deletions

View File

@ -201,12 +201,9 @@ Item {
if (d.activeTestCase < d.openPairTestCase)
return
let items = InspectionUtils.findVisualsByTypeName(dappsWorkflow, "DAppsListPopup")
if (items.length === 1) {
let buttons = InspectionUtils.findVisualsByTypeName(items[0], "StatusButton")
if (buttons.length === 1) {
buttons[0].clicked()
}
let buttons = InspectionUtils.findVisualsByTypeName(dappsWorkflow.popup, "StatusButton")
if (buttons.length === 1) {
buttons[0].clicked()
}
}
@ -232,7 +229,7 @@ Item {
let modals = InspectionUtils.findVisualsByTypeName(dappsWorkflow, "PairWCModal")
if (modals.length === 1) {
let buttons = InspectionUtils.findVisualsByTypeName(modals[0].footer, "StatusButton")
if (buttons.length === 1 && walletConnectService.wcSDK.sdkReady) {
if (buttons.length === 1 && buttons[0].enabled && walletConnectService.wcSDK.sdkReady) {
d.activeTestCase = d.noTestCase
buttons[0].clicked()
return
@ -473,7 +470,7 @@ Item {
property bool testNetworks: false
property bool enableSDK: true
property bool pending : false
property string customAccounts: ""
property string customAccounts: "[]"
property string persistedSessions: "[]"
}

View File

@ -47,8 +47,12 @@ DappsComboBox {
onClosed: pairWCLoader.active = false
onPair: (uri) => {
root.wcService.pair(uri)
this.isPairing = true
root.wcService.pair(uri)
}
onPairUriChanged: (uri) => {
root.wcService.validatePairingUri(uri)
}
}
}
@ -151,8 +155,10 @@ DappsComboBox {
function onMaxFeesUpdated(maxFees, maxFeesWei, haveEnoughFunds, symbol) {
maxFeesText = `${maxFees.toFixed(2)} ${symbol}`
var ethStr = "?"
if (globalUtils) {
try {
ethStr = globalUtils.wei2Eth(maxFeesWei, 9)
} catch (e) {
// ignore error in case of tests and storybook where we don't have access to globalUtils
}
maxFeesEthText = `${ethStr} ETH`
enoughFunds = haveEnoughFunds
@ -179,6 +185,12 @@ DappsComboBox {
Connections {
target: root.wcService
function onPairingUriValidated(validationState) {
if (pairWCLoader.item) {
pairWCLoader.item.pairingUriValidated(validationState)
}
}
function onConnectDApp(dappChains, sessionProposal, availableNamespaces) {
connectDappLoader.dappChains = dappChains
connectDappLoader.sessionProposal = sessionProposal

View File

@ -45,6 +45,38 @@ QObject {
}
readonly property var flatNetworks: root.walletRootStore.filteredFlatModel
function validatePairingUri(uri) {
if(Helpers.containsOnlyEmoji(uri)) {
root.pairingUriValidated(Pairing.uriErrors.tooCool)
return
} else if(!Helpers.validURI(uri)) {
root.pairingUriValidated(Pairing.uriErrors.invalidUri)
return
}
let info = Helpers.extractInfoFromPairUri(uri)
wcSDK.getActiveSessions((sessions) => {
// Check if the URI is already paired
var validationState = Pairing.uriErrors.ok
for (let key in sessions) {
if (sessions[key].pairingTopic == info.topic) {
validationState = Pairing.uriErrors.alreadyUsed
break
}
}
// Check if expired
if (validationState == Pairing.uriErrors.ok) {
const now = (new Date().getTime())/1000
if (info.expiry < now) {
validationState = Pairing.uriErrors.expired
}
}
root.pairingUriValidated(validationState)
});
}
function pair(uri) {
d.acceptedSessionProposal = null
wcSDK.pair(uri)
@ -84,6 +116,7 @@ QObject {
signal approveSessionResult(var session, var error)
signal sessionRequest(SessionRequestResolved request)
signal displayToastMessage(string message, bool error)
signal pairingUriValidated(int validationState)
readonly property Connections sdkConnections: Connections {
target: wcSDK

View File

@ -66,4 +66,37 @@ function buildSupportedNamespaces(chainIds, addresses, methods) {
let methodsStr = methods.map(method => `"${method}"`).join(',')
return `{
"eip155":{"chains": [${eipChainIds.join(',')}],"methods": [${methodsStr}],"events": ["accountsChanged", "chainChanged"],"accounts": [${eipAddresses.join(',')}]}}`
}
function validURI(uri) {
var regex = /^wc:[0-9a-fA-F-]*@([1-9][0-9]*)(\?([a-zA-Z-]+=[^&]+)(&[a-zA-Z-]+=[^&]+)*)?$/
return regex.test(uri)
}
function containsOnlyEmoji(uri) {
var emojiRegex = new RegExp("[\\u203C-\\u3299\\u1F000-\\u1F644]");
return !emojiRegex.test(uri);
}
function extractInfoFromPairUri(uri) {
var topic = ""
var expiry = NaN
// Extract topic and expiry from wc:99fdcac5cc081ac8c1181b4c38c5dc49fb5eb212706d5c94c445be549765e7f0@2?expiryTimestamp=1720090818&relay-protocol=irn&symKey=c6b67d94174bd42d16ff288220ce9b8966e5b56a2d3570a30d5b0a760f1953f0
const regex = /wc:([0-9a-fA-F]*)/
const match = uri.match(regex)
if (match) {
topic = match[1]
}
var parts = uri.split('?')
if (parts.length > 1) {
var params = parts[1].split('&')
for (let i = 0; i < params.length; i++) {
var keyVal = params[i].split('=')
if (keyVal[0] === 'expiryTimestamp') {
expiry = parseInt(keyVal[1])
}
}
}
return { topic, expiry }
}

View File

@ -0,0 +1,14 @@
pragma Singleton
import QtQml 2.15
QtObject {
readonly property QtObject uriErrors: QtObject {
readonly property int notChecked: 0
readonly property int ok: 1
readonly property int tooCool: 2
readonly property int invalidUri: 3
readonly property int alreadyUsed: 4
readonly property int expired: 5
}
}

View File

@ -8,7 +8,7 @@ QtObject {
/// Supported methods
/// userString is used in the context `dapp.url #{userString} <accepted/rejected>`
/// requestDisplay is used in the context `dApp wants you to ${requestDisplay} with <Account Name Here>`
property QtObject methods: QtObject {
readonly property QtObject methods: QtObject {
readonly property QtObject personalSign: QtObject {
readonly property string name: Constants.personal_sign
readonly property string userString: qsTr("sign")

View File

@ -1,2 +1,3 @@
SessionRequestResolved 1.0 SessionRequestResolved.qml
singleton SessionRequest 1.0 SessionRequest.qml
singleton SessionRequest 1.0 SessionRequest.qml
singleton Pairing 1.0 Pairing.qml

View File

@ -13,6 +13,8 @@ import utils 1.0
import shared.controls 1.0
import shared.popups 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import "PairWCModal"
StatusDialog {
@ -25,7 +27,15 @@ StatusDialog {
property bool isPairing: false
function pairingUriValidated(validationState) {
uriInput.errorState = validationState
if (validationState === Pairing.uriErrors.ok) {
d.doPair()
}
}
signal pair(string uri)
signal pairUriChanged(string uri)
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
@ -41,7 +51,12 @@ StatusDialog {
WCUriInput {
id: uriInput
onTextChanged: root.isPairing = false
pending: uriInput.errorState === Pairing.uriErrors.notChecked
onTextChanged: {
root.isPairing = false
root.pairUriChanged(uriInput.text)
}
}
// Spacer
@ -73,10 +88,24 @@ StatusDialog {
height: 44
text: qsTr("Done")
enabled: uriInput.valid && !root.isPairing && uriInput.text.length > 0
enabled: uriInput.valid
&& !root.isPairing
&& uriInput.text.length > 0
&& uriInput.errorState === Pairing.uriErrors.ok
onClicked: root.pair(uriInput.text)
onClicked: {
d.doPair()
}
}
}
}
QtObject {
id: d
function doPair() {
root.isPairing = true
root.pair(uriInput.text)
}
}
}

View File

@ -9,12 +9,15 @@ import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import AppLayouts.Wallet.services.dapps.types 1.0
ColumnLayout {
id: root
readonly property bool valid: input.valid && input.text.length > 0
readonly property alias text: input.text
property alias pending: input.pending
property int errorState: Pairing.uriErrors.notChecked
StatusBaseInput {
id: input
@ -29,49 +32,27 @@ ColumnLayout {
valid: {
let uri = input.text
errorText.text = ""
if(uri.length === 0) {
errorText.text = ""
return true
}
if(containsOnlyEmoji(uri)) {
if(root.errorState === Pairing.uriErrors.tooCool) {
errorText.text = qsTr("WalletConnect URI too cool")
return false
} else if(!validURI(uri)) {
} else if(root.errorState === Pairing.uriErrors.invalidUri) {
errorText.text = qsTr("WalletConnect URI invalid")
return false
} else if(wcUriAlreadyUsed(uri)) {
} else if(root.errorState === Pairing.uriErrors.alreadyUsed) {
errorText.text = qsTr("WalletConnect URI already used")
return false
} else if(wcUriExpired(uri)) {
} else if(root.errorState === Pairing.uriErrors.expired) {
errorText.text = qsTr("WalletConnect URI has expired")
}
if (errorText.text.length > 0) {
return false
}
errorText.text = ""
return true
}
function validURI(uri) {
var regex = /^wc:[0-9a-fA-F-]*@([1-9][0-9]*)(\?([a-zA-Z-]+=[^&]+)(&[a-zA-Z-]+=[^&]+)*)?$/
return regex.test(uri)
}
function containsOnlyEmoji(uri) {
var emojiRegex = new RegExp("[\\u203C-\\u3299\\u1F000-\\u1F644]");
return !emojiRegex.test(uri);
}
function wcUriAlreadyUsed(uri) {
// TODO: Check if URI is already used
return false
}
function wcUriExpired(uri) {
// TODO: Check if URI is expired
return false
}
rightComponent: Item {
width: pasteButton.implicitWidth
height: pasteButton.implicitHeight