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