From 15b2c084d2f004352c06ddcad595914a0bacc685 Mon Sep 17 00:00:00 2001 From: Stefan Date: Fri, 5 Jul 2024 12:32:31 +0300 Subject: [PATCH] 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 --- storybook/pages/DAppsWorkflowPage.qml | 13 +++---- .../Wallet/panels/DAppsWorkflow.qml | 16 +++++++- .../services/dapps/WalletConnectService.qml | 33 ++++++++++++++++ .../Wallet/services/dapps/helpers.js | 33 ++++++++++++++++ .../Wallet/services/dapps/types/Pairing.qml | 14 +++++++ .../services/dapps/types/SessionRequest.qml | 2 +- .../Wallet/services/dapps/types/qmldir | 3 +- .../popups/walletconnect/PairWCModal.qml | 35 +++++++++++++++-- .../walletconnect/PairWCModal/WCUriInput.qml | 39 +++++-------------- 9 files changed, 144 insertions(+), 44 deletions(-) create mode 100644 ui/app/AppLayouts/Wallet/services/dapps/types/Pairing.qml diff --git a/storybook/pages/DAppsWorkflowPage.qml b/storybook/pages/DAppsWorkflowPage.qml index b154f9d4cb..51102e841b 100644 --- a/storybook/pages/DAppsWorkflowPage.qml +++ b/storybook/pages/DAppsWorkflowPage.qml @@ -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: "[]" } diff --git a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml index 2c22178f79..0078ef7d87 100644 --- a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml +++ b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml @@ -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 diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml index 46676030f6..3461e276a4 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml @@ -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 diff --git a/ui/app/AppLayouts/Wallet/services/dapps/helpers.js b/ui/app/AppLayouts/Wallet/services/dapps/helpers.js index 312dd46d92..7925d235d4 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/helpers.js +++ b/ui/app/AppLayouts/Wallet/services/dapps/helpers.js @@ -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 } } \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/types/Pairing.qml b/ui/app/AppLayouts/Wallet/services/dapps/types/Pairing.qml new file mode 100644 index 0000000000..b64901a046 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/services/dapps/types/Pairing.qml @@ -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 + } +} \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequest.qml b/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequest.qml index 9996571306..80851508c8 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequest.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequest.qml @@ -8,7 +8,7 @@ QtObject { /// Supported methods /// userString is used in the context `dapp.url #{userString} ` /// requestDisplay is used in the context `dApp wants you to ${requestDisplay} with ` - 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") diff --git a/ui/app/AppLayouts/Wallet/services/dapps/types/qmldir b/ui/app/AppLayouts/Wallet/services/dapps/types/qmldir index dd6d6c012e..ae2bf029da 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/types/qmldir +++ b/ui/app/AppLayouts/Wallet/services/dapps/types/qmldir @@ -1,2 +1,3 @@ SessionRequestResolved 1.0 SessionRequestResolved.qml -singleton SessionRequest 1.0 SessionRequest.qml \ No newline at end of file +singleton SessionRequest 1.0 SessionRequest.qml +singleton Pairing 1.0 Pairing.qml \ No newline at end of file diff --git a/ui/imports/shared/popups/walletconnect/PairWCModal.qml b/ui/imports/shared/popups/walletconnect/PairWCModal.qml index 80ed7c8bcf..6cd37cd8f5 100644 --- a/ui/imports/shared/popups/walletconnect/PairWCModal.qml +++ b/ui/imports/shared/popups/walletconnect/PairWCModal.qml @@ -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) + } + } } diff --git a/ui/imports/shared/popups/walletconnect/PairWCModal/WCUriInput.qml b/ui/imports/shared/popups/walletconnect/PairWCModal/WCUriInput.qml index cefbe272a7..6c60e08d06 100644 --- a/ui/imports/shared/popups/walletconnect/PairWCModal/WCUriInput.qml +++ b/ui/imports/shared/popups/walletconnect/PairWCModal/WCUriInput.qml @@ -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