diff --git a/src/app/modules/shared_modules/connector/controller.nim b/src/app/modules/shared_modules/connector/controller.nim index 3b3896452a..8dd685e0c9 100644 --- a/src/app/modules/shared_modules/connector/controller.nim +++ b/src/app/modules/shared_modules/connector/controller.nim @@ -34,8 +34,8 @@ QtObject: proc approveConnectResponse*(self: Controller, payload: string, error: bool) {.signal.} proc rejectConnectResponse*(self: Controller, payload: string, error: bool) {.signal.} - proc approveTransactionResponse*(self: Controller, requestId: string, error: bool) {.signal.} - proc rejectTransactionResponse*(self: Controller, requestId: string, error: bool) {.signal.} + proc approveTransactionResponse*(self: Controller, topic: string, requestId: string, error: bool) {.signal.} + proc rejectTransactionResponse*(self: Controller, topic: string, requestId: string, error: bool) {.signal.} proc newController*(service: connector_service.Service, events: EventEmitter): Controller = new(result, delete) @@ -114,15 +114,15 @@ QtObject: result = self.service.rejectDappConnect(requestId) self.rejectConnectResponse(requestId, not result) - proc approveTransaction*(self: Controller, requestId: string, signature: string): bool {.slot.} = + proc approveTransaction*(self: Controller, sessionTopic: string, requestId: string, signature: string): bool {.slot.} = let hash = utils.createHash(signature) result = self.service.approveTransactionRequest(requestId, hash) - self.approveTransactionResponse(requestId, not result) + self.approveTransactionResponse(sessionTopic, requestId, not result) - proc rejectTransaction*(self: Controller, requestId: string): bool {.slot.} = + proc rejectTransaction*(self: Controller, sessionTopic: string, requestId: string): bool {.slot.} = result = self.service.rejectTransactionSigning(requestId) - self.rejectTransactionResponse(requestId, not result) + self.rejectTransactionResponse(sessionTopic, requestId, not result) proc disconnect*(self: Controller, dAppUrl: string): bool {.slot.} = result = self.service.recallDAppPermission(dAppUrl) diff --git a/storybook/pages/DAppsWorkflowPage.qml b/storybook/pages/DAppsWorkflowPage.qml index 3bb9a8802d..dee3c954d6 100644 --- a/storybook/pages/DAppsWorkflowPage.qml +++ b/storybook/pages/DAppsWorkflowPage.qml @@ -6,6 +6,7 @@ import Qt.labs.settings 1.0 import QtTest 1.15 import QtQml.Models 2.14 +import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Backpressure 0.1 import StatusQ.Controls 0.1 @@ -64,13 +65,13 @@ Item { spacing: 8 - readonly property var wcService: walletConnectService + readonly property var wcService: dappsService loginType: Constants.LoginType.Biometrics selectedAccountAddress: "" model: wcService.dappsModel - accountsModel: wcService.validAccounts - networksModel: wcService.flatNetworks + accountsModel: dappModule.accountsModel + networksModel: dappModule.networksModel sessionRequestsModel: wcService.sessionRequestsModel enabled: wcService.isServiceOnline @@ -127,7 +128,7 @@ Item { Layout.preferredWidth: 20 Layout.preferredHeight: Layout.preferredWidth radius: Layout.preferredWidth / 2 - color: walletConnectService.wcSDK.sdkReady ? "green" : "red" + color: dappModule.wcSdk.sdkReady ? "green" : "red" } } @@ -166,7 +167,7 @@ Item { ListView { Layout.fillWidth: true Layout.preferredHeight: Math.min(50, contentHeight) - model: walletConnectService.sessionRequestsModel + model: dappsService.sessionRequestsModel delegate: RowLayout { StatusBaseText { text: SQUtils.Utils.elideAndFormatWalletAddress(model.topic, 6, 4) @@ -208,7 +209,7 @@ Item { NetworkFilter { id: networkFilter - flatNetworks: walletConnectService.walletRootStore.filteredFlatModel + flatNetworks: dappModule.networksModel } // spacer @@ -228,7 +229,7 @@ Item { text: "WC feature flag" checked: true onCheckedChanged: { - walletConnectService.walletConnectFeatureEnabled = checked + dappsService.walletConnectFeatureEnabled = checked } } @@ -236,7 +237,7 @@ Item { text: "Connector feature flag" checked: true onCheckedChanged: { - walletConnectService.connectorFeatureEnabled = checked + dappsService.connectorFeatureEnabled = checked } } } @@ -307,7 +308,7 @@ Item { let modals = StoryBook.InspectionUtils.findVisualsByTypeName(dappsWorkflow, "PairWCModal") if (modals.length === 1) { let buttons = StoryBook.InspectionUtils.findVisualsByTypeName(modals[0].footer, "StatusButton") - if (buttons.length === 1 && buttons[0].enabled && walletConnectService.wcSDK.sdkReady) { + if (buttons.length === 1 && buttons[0].enabled && dappModule.wcSdk.sdkReady) { d.activeTestCase = d.noTestCase buttons[0].clicked() return @@ -342,14 +343,14 @@ Item { StatusButton { text: qsTr("Reject") onClicked: { - walletConnectService.store.userAuthenticationFailed(authMockDialog.topic, authMockDialog.id) + dappModule.store.userAuthenticationFailed(authMockDialog.topic, authMockDialog.id) authMockDialog.close() } } StatusButton { text: qsTr("Authenticate") onClicked: { - walletConnectService.store.userAuthenticated(authMockDialog.topic, authMockDialog.id, "0x1234567890", "123") + dappModule.store.userAuthenticated(authMockDialog.topic, authMockDialog.id, "0x1234567890", "123") authMockDialog.close() } } @@ -357,17 +358,35 @@ Item { } } - WalletConnectService { - id: walletConnectService - - wcSDK: WalletConnectSDK { - enableSdk: settings.enableSDK + DAppsModule { + id: dappModule + wcSdk: WalletConnectSDK { + enabled: settings.enableSDK && dappsService.walletConnectFeatureEnabled projectId: projectIdText.projectId } - blockchainNetworksDown: networkFilter.selection + bcSdk: DappsConnectorSDK { + enabled: false + projectId: projectIdText.projectId + networksModel: dappModule.networksModel + accountsModel: dappModule.accountsModel + store: SharedStores.BrowserConnectStore { + signal connectRequested(string requestId, string dappJson) + signal signRequested(string requestId, string requestJson) + + signal connected(string dappJson) + signal disconnected(string dappJson) + + // Responses to user actions + signal approveConnectResponse(string id, bool error) + signal rejectConnectResponse(string id, bool error) + + signal approveTransactionResponse(string topic, string requestId, bool error) + signal rejectTransactionResponse(string topic, string requestId, bool error) + } + } store: SharedStores.DAppsStore { signal dappsListReceived(string dappsJson) signal userAuthenticated(string topic, string id, string password, string pin) @@ -485,25 +504,29 @@ Item { } } - walletRootStore: SQUtils.QObject { - property var filteredFlatModel: SortFilterProxyModel { - sourceModel: NetworksModel.flatNetworks - filters: ValueFilter { roleName: "isTest"; value: settings.testNetworks; } - } - property var accounts: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel - readonly property ListModel nonWatchAccounts: accounts + currenciesStore: SharedStores.CurrenciesStore {} + groupedAccountAssetsModel: GroupedAccountsAssetsModel {} + accountsModel: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel - readonly property SharedStores.CurrenciesStore currencyStore: SharedStores.CurrenciesStore {} - readonly property WalletStore.WalletAssetsStore walletAssetsStore: WalletStore.WalletAssetsStore { - // Silence warnings - assetsWithFilteredBalances: ListModel {} - // Name mismatch between storybook and production - readonly property var groupedAccountAssetsModel: groupedAccountsAssetsModel - } - - readonly property string selectedAddress: "" + networksModel: SortFilterProxyModel { + sourceModel: NetworksModel.flatNetworks + proxyRoles: [ + FastExpressionRole { + name: "isOnline" + expression: !networkFilter.selection.map(Number).includes(model.chainId) + expectedRoles: "chainId" + } + ] + filters: ValueFilter { roleName: "isTest"; value: settings.testNetworks; } } + } + DAppsService { + id: dappsService + + dappsModule: dappModule + accountsModel: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel + selectedAddress: "" onDisplayToastMessage: (message, isErr) => { if(isErr) { console.log(`Storybook.displayToastMessage(${message}, "", "warning", false, Constants.ephemeralNotificationType.danger, "")`) diff --git a/storybook/pages/SessionRequestPage.qml b/storybook/pages/SessionRequestPage.qml new file mode 100644 index 0000000000..bd849e871c --- /dev/null +++ b/storybook/pages/SessionRequestPage.qml @@ -0,0 +1,103 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import AppLayouts.Wallet.services.dapps.types 1.0 + +SplitView { + id: root + orientation: Qt.Horizontal + + readonly property string sign: "{\n\ + \"id\": 1730473461432473,\n\ + \"params\": {\n\ + \"chainId\": \"eip155:1\",\n\ + \"request\": {\n\ + \"expiryTimestamp\": 1730473761,\n\ + \"method\": \"personal_sign\",\n\ + \"params\": [\n\ + \"0x4d7920656d61696c206973206a6f686e40646f652e636f6d202d2031373330343733343631343331\",\n\ + \"0x8b6950bb8a74489a83e6a1281e3aa008f02bf368\"\n\ + ]\n\ + },\n\ + \"topic\": \"3a9a320f8fc8e7a814895b148911373ba7df58c176ddca989f0e72ea1f9b8148\",\n\ + \"verifyContext\": {\n\ + \"verified\": {\n\ + \"isScam\": false,\n\ + \"origin\": \"https://react-app.walletconnect.com\",\n\ + \"validation\": \"VALID\",\n\ + \"verifyUrl\": \"https://verify.walletconnect.org\"\n\ + }\n\ + }\n\ + }\n\ + }" + readonly property string transaction: "{\n\ + \"id\": 1730473547658704,\n\ + \"params\": {\n\ + \"chainId\": \"eip155:10\",\n\ + \"request\": {\n\ + \"expiryTimestamp\": 1730473847,\n\ + \"method\": \"eth_sendTransaction\",\n\ + \"params\": [\n\ + {\n\ + \"data\": \"0x\",\n\ + \"from\": \"0x8b6950bb8a74489a83e6a1281e3aa008f02bf368\",\n\ + \"gasLimit\": \"0x5208\",\n\ + \"gasPrice\": \"0x0f437c\",\n\ + \"nonce\": \"0x4e\",\n\ + \"to\": \"0x8b6950bb8a74489a83e6a1281e3aa008f02bf368\",\n\ + \"value\": \"0x00\"\n\ + }\n\ + ]\n\ + }\n\ + },\n\ + \"topic\": \"3a9a320f8fc8e7a814895b148911373ba7df58c176ddca989f0e72ea1f9b8148\",\n\ + \"verifyContext\": {\n\ + \"verified\": {\n\ + \"isScam\": false,\n\ + \"origin\": \"https://react-app.walletconnect.com\",\n\ + \"validation\": \"VALID\",\n\ + \"verifyUrl\": \"https://verify.walletconnect.org\"\n\ + }\n\ + }\n\ + }" + ScrollView { + SplitView.fillHeight: true + SplitView.fillWidth: true + TextArea { + id: result + text: "Result: " + JSON.stringify(SessionRequest.parse(JSON.parse(textEdit.text.replace(/\\n/g, "\n"))), undefined, 2) + readOnly: true + } + } + + ColumnLayout { + SplitView.fillHeight: true + SplitView.fillWidth: true + SplitView.preferredWidth: root.width / 2 + Label { + text: "Paste the event here to simulate the session request parsing" + font.bold: true + } + Rectangle { + Layout.fillWidth: true + height: 2 + color: "black" + } + TextArea { + id: textEdit + Layout.fillHeight: true + Layout.fillWidth: true + text: root.transaction + onTextChanged: text = JSON.stringify(JSON.parse(text.replace(/\\/g, "")), undefined, 2) + } + ComboBox { + id: comboBox + Layout.fillWidth: true + Layout.fillHeight: true + model: ["sign", "transaction"] + currentIndex: 0 + onCurrentIndexChanged: textEdit.text = root[comboBox.currentText] + } + } +} \ No newline at end of file diff --git a/storybook/qmlTests/tests/tst_BCDAppsProvider.qml b/storybook/qmlTests/tests/tst_BCDAppsProvider.qml new file mode 100644 index 0000000000..032669e6db --- /dev/null +++ b/storybook/qmlTests/tests/tst_BCDAppsProvider.qml @@ -0,0 +1,98 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import AppLayouts.Wallet.services.dapps 1.0 + +Item { + id: root + + Component { + id: bcDAppsProviderComponent + BCDappsProvider { + id: bcDAppsProvider + readonly property SignalSpy connectedSpy: SignalSpy { target: bcDAppsProvider; signalName: "connected" } + readonly property SignalSpy disconnectedSpy: SignalSpy { target: bcDAppsProvider; signalName: "disconnected" } + bcSDK: WalletConnectSDKBase { + enabled: true + projectId: "" + property var activeSessions: {} + getActiveSessions: function(callback) { + callback(activeSessions) + } + } + } + } + + function buildSession(dappUrl, dappName, dappIcon, proposalId, account, chains) { + let sessionTemplate = (dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains) => { + return { + peer: { + metadata: { + description: "-", + icons: [ + dappIcon + ], + name: dappName, + url: dappUrl + } + }, + namespaces: { + eip155: { + accounts: [eipAccount], + chains: eipChains + } + }, + pairingTopic: proposalId, + topic: dappUrl + }; + } + + const eipAccount = account ? `eip155:${account}` : "" + const eipChains = chains ? chains.map((chain) => `eip155:${chain}`) : [] + + return sessionTemplate(dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains) + } + + TestCase { + id: bcDAppsProviderTest + + property BCDappsProvider componentUnderTest: null + function init() { + componentUnderTest = createTemporaryObject(bcDAppsProviderComponent, root) + } + + function test_addRemoveSession() { + const newSession = buildSession("https://example.com", "Example", "https://example.com/icon.png", "123", "0x123", ["1"]) + componentUnderTest.bcSDK.approveSessionResult("requestID", newSession, null) + + compare(componentUnderTest.connectedSpy.count, 1, "Connected signal should be emitted once") + compare(componentUnderTest.connectedSpy.signalArguments[0][0], "requestID", "Connected signal should have correct proposalId") + compare(componentUnderTest.connectedSpy.signalArguments[0][1], "https://example.com", "Connected signal should have correct topic") + compare(componentUnderTest.connectedSpy.signalArguments[0][2], "https://example.com", "Connected signal should have correct dAppUrl") + + const dapp = componentUnderTest.getByTopic("https://example.com") + verify(!!dapp, "DApp should be found") + compare(dapp.name, "Example", "DApp should have correct name") + compare(dapp.url, "https://example.com", "DApp should have correct url") + compare(dapp.iconUrl, "https://example.com/icon.png", "DApp should have correct iconUrl") + compare(dapp.topic, "https://example.com", "DApp should have correct topic") + compare(dapp.connectorId, componentUnderTest.connectorId, "DApp should have correct connectorId") + compare(dapp.accountAddresses.count, 1, "DApp should have correct accountAddresses count") + compare(dapp.accountAddresses.get(0).address, "0x123", "DApp should have correct accountAddresses address") + compare(dapp.rawSessions.count, 1, "DApp should have correct rawSessions count") + + componentUnderTest.bcSDK.sessionDelete("https://example.com", "") + compare(componentUnderTest.disconnectedSpy.count, 1, "Disconnected signal should be emitted once") + compare(componentUnderTest.disconnectedSpy.signalArguments[0][0], "https://example.com", "Disconnected signal should have correct topic") + compare(componentUnderTest.disconnectedSpy.signalArguments[0][1], "https://example.com", "Disconnected signal should have correct dAppUrl") + } + + function test_disabledSDK() { + componentUnderTest.bcSDK.enabled = false + componentUnderTest.bcSDK.approveSessionResult("requestID", buildSession("https://example.com", "Example", "https://example.com/icon.png", "123", "0x123", ["1"]), "") + compare(componentUnderTest.connectedSpy.count, 0, "Connected signal should not be emitted") + componentUnderTest.bcSDK.sessionDelete("https://example.com", "") + compare(componentUnderTest.disconnectedSpy.count, 0, "Disconnected signal should not be emitted") + } + } +} \ No newline at end of file diff --git a/storybook/qmlTests/tests/tst_DAppsWorkflow.qml b/storybook/qmlTests/tests/tst_DAppsWorkflow.qml index 6ddf86ed70..d42c72869b 100644 --- a/storybook/qmlTests/tests/tst_DAppsWorkflow.qml +++ b/storybook/qmlTests/tests/tst_DAppsWorkflow.qml @@ -25,19 +25,31 @@ Item { width: 600 height: 400 + function mockActiveSession(accountsModel, networksModel, sdk, topic) { + const account = accountsModel.get(1) + const networks = ModelUtils.modelToFlatArray(networksModel, "chainId") + const requestId = 1717149885151715 + const session = JSON.parse(Testing.formatApproveSessionResponse(networks, [account.address])) + const sessionProposal = JSON.parse(Testing.formatSessionProposal()) + + sdk.sessionProposal(sessionProposal) + // Expect to have calls to getActiveSessions from service initialization + const prevRequests = sdk.getActiveSessionsCallbacks.length + sdk.approveSessionResult(sessionProposal.id, session, null) + // Service might trigger a sessionRequest event following the getActiveSessions call + const callback = sdk.getActiveSessionsCallbacks[prevRequests].callback + callback({"b536a": session}) + + return session + } + function mockSessionRequestEvent(tc, sdk, accountsModel, networksModel) { const account = accountsModel.get(1) const network = networksModel.get(1) const topic = "b536a" const requestId = 1717149885151715 + const session = mockActiveSession(accountsModel, networksModel, sdk, topic) const request = buildSessionRequestResolved(tc, account.address, network.chainId, topic, requestId) - // Expect to have calls to getActiveSessions from service initialization - const prevRequests = sdk.getActiveSessionsCallbacks.length - sdk.sessionRequestEvent(request.event) - // Service might trigger a sessionRequest event following the getActiveSessions call - const callback = sdk.getActiveSessionsCallbacks[prevRequests].callback - const session = JSON.parse(Testing.formatApproveSessionResponse([network.chainId, 7], [account.address])) - callback({"b536a": session}) return {sdk, session, account, network, topic, request} } @@ -58,10 +70,12 @@ Item { data: "hello world", preparedData: "hello world", expirationTimestamp: (Date.now() + 10000) / 1000, - sourceId: Constants.DAppConnectors.WalletConnect + sourceId: Constants.DAppConnectors.WalletConnect, + dappName: "Test DApp", + dappUrl: "https://test.dapp", + dappIcon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAcCAYAAACdz7SqAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAM2SURBVHgBtVbNbtNAEJ7ZpBQ4pRGF9kQqWqkBRNwnwLlxI9y4NX2CiiOntE9QeINw49a8QdwT3NhKQCKaSj4WUVXmABRqe5hxE+PGTuyk5ZOSXe/ftzs/3y5CBiw/NEzw/cdAaCJAifgXdCA4QGAjggbEvbMf0LJt7aSth6lkHjW4akIG8GI2/1k5H7e7XW2PGRdHqWQU8jdoNytZIrnC7YNPupnUnxtuWF01SjhD77hqwPQosNlrxdt34OTb172xpELoKvrA1QW4EqCZRJyLEnpI7ZBQggThlGvXYVLI3HAeE88vfj85Pno/6FaDiqeoEUZlMA9bvc/7cxyxVa6/SeM5j2Tcdn/hnHsNly520s7KAyN0V17+7pWNGhHVhxYJTNLraosLi8e0kMBxT0FH00IW830oeT/ButBertjRQ5BPO1xUQ1IE2oQUHHZ0K6mdI1RzoSEdpqRg76O2lPgSElKDdz919JYMoxA95QDow7qUykWoxTo5z2YIXsGUsLV2CPD1cDu7MODiQKKnsVmI1jhFyQJvFrb6URxFQWJAYYIZSEF6tKZATitFQpehEm1PkCraWYCE+8Nt5ENBwX8EAd2NNaKQxu0ukVuCqwATQHwnjhphShMuiSAVKZ527E6bzYt78Q3SulxvcAm44K8ntXMqagmkJDUpzNwMZGsqBDqLuDXcLvkvqajcWWgm+ZUI6svlym5fsbITlh9tsgi0Ezs5//vkMtBocqSJOZw84ZrHPiXFJ6UwECx5A/FbqNXX2hAiefkzqCNRha1Wi8yJgddeCk4qHzkK1aMgdypfshYRbkTGm3z0Rs6LW0REgDXVEMuMI0TE5kDlgkv8+PjIKRYXfzPxEyH2EYzDzv7L4q1FHsvpg8Gkt186OlGp5uYXZMjzkYS8txwfQnj63//APmzDIF1yWJVrCDJgeZVfjTjCj0KicC3qlny0053FZ/k/PFnyy6P2yv1Kk1T/1eCGF/pEYCncGI6DCzIo/uGnRvg8CfzE5MEPoQGT4Pz5Uj3oxp+hMe0V4oOOrssOMfmWyMJo5X1cG2WZkYIvO2Tn85sGXwg5B5Q9kiKMas5DntPr6Oq4+/gvs8hkkbAzoC8AAAAASUVORK5CYII=" }) - requestItem.resolveDappInfoFromSession({peer: {metadata: {name: "Test DApp", url: "https://test.dapp", icons: ["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAcCAYAAACdz7SqAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAM2SURBVHgBtVbNbtNAEJ7ZpBQ4pRGF9kQqWqkBRNwnwLlxI9y4NX2CiiOntE9QeINw49a8QdwT3NhKQCKaSj4WUVXmABRqe5hxE+PGTuyk5ZOSXe/ftzs/3y5CBiw/NEzw/cdAaCJAifgXdCA4QGAjggbEvbMf0LJt7aSth6lkHjW4akIG8GI2/1k5H7e7XW2PGRdHqWQU8jdoNytZIrnC7YNPupnUnxtuWF01SjhD77hqwPQosNlrxdt34OTb172xpELoKvrA1QW4EqCZRJyLEnpI7ZBQggThlGvXYVLI3HAeE88vfj85Pno/6FaDiqeoEUZlMA9bvc/7cxyxVa6/SeM5j2Tcdn/hnHsNly520s7KAyN0V17+7pWNGhHVhxYJTNLraosLi8e0kMBxT0FH00IW830oeT/ButBertjRQ5BPO1xUQ1IE2oQUHHZ0K6mdI1RzoSEdpqRg76O2lPgSElKDdz919JYMoxA95QDow7qUykWoxTo5z2YIXsGUsLV2CPD1cDu7MODiQKKnsVmI1jhFyQJvFrb6URxFQWJAYYIZSEF6tKZATitFQpehEm1PkCraWYCE+8Nt5ENBwX8EAd2NNaKQxu0ukVuCqwATQHwnjhphShMuiSAVKZ527E6bzYt78Q3SulxvcAm44K8ntXMqagmkJDUpzNwMZGsqBDqLuDXcLvkvqajcWWgm+ZUI6svlym5fsbITlh9tsgi0Ezs5//vkMtBocqSJOZw84ZrHPiXFJ6UwECx5A/FbqNXX2hAiefkzqCNRha1Wi8yJgddeCk4qHzkK1aMgdypfshYRbkTGm3z0Rs6LW0REgDXVEMuMI0TE5kDlgkv8+PjIKRYXfzPxEyH2EYzDzv7L4q1FHsvpg8Gkt186OlGp5uYXZMjzkYS8txwfQnj63//APmzDIF1yWJVrCDJgeZVfjTjCj0KicC3qlny0053FZ/k/PFnyy6P2yv1Kk1T/1eCGF/pEYCncGI6DCzIo/uGnRvg8CfzE5MEPoQGT4Pz5Uj3oxp+hMe0V4oOOrssOMfmWyMJo5X1cG2WZkYIvO2Tn85sGXwg5B5Q9kiKMas5DntPr6Oq4+/gvs8hkkbAzoC8AAAAASUVORK5CYII="]}}}) return requestItem } @@ -70,6 +84,7 @@ Item { WalletConnectSDKBase { property bool sdkReady: true + enabled: true property var getActiveSessionsCallbacks: [] getActiveSessions: function(callback) { @@ -106,7 +121,7 @@ Item { Component { id: serviceComponent - WalletConnectService { + DAppsService { property var onApproveSessionResultTriggers: [] onApproveSessionResult: function(session, error) { onApproveSessionResultTriggers.push({session, error}) @@ -121,7 +136,6 @@ Item { onPairingValidated: function(validationState) { onPairingValidatedTriggers.push({validationState}) } - blockchainNetworksDown: [] } } @@ -243,7 +257,7 @@ Item { } // Used by tst_balanceCheck ListElement { - chainId: 421613 + chainId: 421614 layer: 2 isOnline: true } @@ -294,37 +308,34 @@ Item { } Component { - id: dappsRequestHandlerComponent + id: dappsModuleComponent - DAppsRequestHandler { + DAppsModule { currenciesStore: CurrenciesStore {} - assetsStore: assetsStoreMock + groupedAccountAssetsModel: assetsStoreMock.groupedAccountAssetsModel } } TestCase { - id: requestHandlerTest - name: "DAppsRequestHandler" + id: dappsModuleTest + name: "DAppsModuleTest" // Ensure mocked GroupedAccountsAssetsModel is properly initialized when: windowShown - property DAppsRequestHandler handler: null - - SignalSpy { - id: displayToastMessageSpy - target: requestHandlerTest.handler - signalName: "onDisplayToastMessage" - } + property DAppsModule handler: null function init() { let walletStore = createTemporaryObject(walletStoreComponent, root) verify(!!walletStore) - let sdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab" }) + let sdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab", enabled: true }) verify(!!sdk) + let bcSdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab", enabled: false }) + verify(!!bcSdk) let store = createTemporaryObject(dappsStoreComponent, root) verify(!!store) - handler = createTemporaryObject(dappsRequestHandlerComponent, root, { - sdk: sdk, + handler = createTemporaryObject(dappsModuleComponent, root, { + wcSdk: sdk, + bcSdk: bcSdk, store: store, accountsModel: walletStore.nonWatchAccounts, networksModel: walletStore.filteredFlatModelWithOnlineStat @@ -333,13 +344,9 @@ Item { sdk.getActiveSessionsCallbacks = [] } - function cleanup() { - displayToastMessageSpy.clear() - } - function test_TestAuthentication() { - let td = mockSessionRequestEvent(this, handler.sdk, handler.accountsModel, handler.networksModel) - handler.sdk.sessionRequestEvent(td.request.event) + let td = mockSessionRequestEvent(this, handler.wcSdk, handler.accountsModel, handler.networksModel) + handler.wcSdk.sessionRequestEvent(td.request.event) let request = handler.requestsModel.findById(td.request.requestId) request.accept() compare(handler.store.authenticateUserCalls.length, 1, "expected a call to store.authenticateUser") @@ -351,7 +358,7 @@ Item { } function test_onSessionRequestEventDifferentCaseForAddress() { - const sdk = handler.sdk + const sdk = handler.wcSdk const testAddressUpper = "0x3A" const chainId = 2 @@ -362,6 +369,7 @@ Item { const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic)) // Expect to have calls to getActiveSessions from service initialization const prevRequests = sdk.getActiveSessionsCallbacks.length + mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic) sdk.sessionRequestEvent(session) compare(sdk.getActiveSessionsCallbacks.length, 1, "expected DAppsRequestHandler call sdk.getActiveSessions") @@ -369,7 +377,7 @@ Item { // Tests that the request is ignored if not in the current profile (don't have the PK for the address) function test_onSessionRequestEventMissingAddress() { - const sdk = handler.sdk + const sdk = handler.wcSdk const testAddressUpper = "0xY" const chainId = 2 @@ -378,12 +386,13 @@ Item { const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddressUpper}"`] const topic = "b536a" const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic)) - // Expect to have calls to getActiveSessions from service initialization - const prevRequests = sdk.getActiveSessionsCallbacks.length + + mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic) + sdk.getActiveSessionsCallbacks = [] sdk.sessionRequestEvent(session) compare(sdk.getActiveSessionsCallbacks.length, 0, "expected DAppsRequestHandler don't call sdk.getActiveSessions") - compare(sdk.rejectSessionRequestCalls.length, 0, "expected no call to service.rejectSessionRequest") + compare(sdk.rejectSessionRequestCalls.length, 1, "expected to reject the request") } function test_balanceCheck_data() { @@ -434,7 +443,7 @@ Item { } function test_balanceCheck(data) { - let sdk = handler.sdk + let sdk = handler.wcSdk // Override the suggestedFees if (!!data.maxFeePerGasM) { @@ -456,6 +465,7 @@ Item { }`] let topic = "b536a" let session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic)) + mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic) sdk.sessionRequestEvent(session) compare(sdk.getActiveSessionsCallbacks.length, 1, "expected DAppsRequestHandler call sdk.getActiveSessions") @@ -473,7 +483,7 @@ Item { } function test_sessionRequestExpiryInTheFuture() { - const sdk = handler.sdk + const sdk = handler.wcSdk const testAddressUpper = "0x3A" const chainId = 2 const method = "personal_sign" @@ -486,6 +496,7 @@ Item { // Expect to have calls to getActiveSessions from service initialization const prevRequests = sdk.getActiveSessionsCallbacks.length + mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic) sdk.sessionRequestEvent(session) verify(handler.requestsModel.count === 1, "expected a request to be added") @@ -496,7 +507,7 @@ Item { function test_sessionRequestExpiryInThePast() { - const sdk = handler.sdk + const sdk = handler.wcSdk const testAddressUpper = "0x3A" const chainId = 2 const method = "personal_sign" @@ -508,18 +519,18 @@ Item { verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past") + mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic) sdk.sessionRequestEvent(session) verify(handler.requestsModel.count === 1, "expected a request to be added") const request = handler.requestsModel.findRequest(topic, session.id) verify(!!request, "expected request to be found") verify(request.isExpired(), "expected request to be expired") - verify(displayToastMessageSpy.count === 0, "no toast message should be displayed") } function test_wcSignalsSessionRequestExpiry() { - const sdk = handler.sdk + const sdk = handler.wcSdk const testAddressUpper = "0x3A" const chainId = 2 const method = "personal_sign" @@ -529,6 +540,8 @@ Item { const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic)) verify(session.params.request.expiryTimestamp > Date.now() / 1000, "expected expiryTimestamp to be in the future") + + mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic) sdk.sessionRequestEvent(session) const request = handler.requestsModel.findRequest(topic, session.id) verify(!!request, "expected request to be found") @@ -536,12 +549,11 @@ Item { sdk.sessionRequestExpired(session.id) verify(request.isExpired(), "expected request to be expired") - verify(displayToastMessageSpy.count === 0, "no toast message should be displayed") } function test_acceptExpiredSessionRequest() { - const sdk = handler.sdk + const sdk = handler.wcSdk const testAddressUpper = "0x3A" const chainId = 2 const method = "personal_sign" @@ -552,12 +564,12 @@ Item { session.params.request.expiryTimestamp = (Date.now() - 10000) / 1000 verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past") - + + mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic) sdk.sessionRequestEvent(session) verify(handler.requestsModel.count === 1, "expected a request to be added") const request = handler.requestsModel.findRequest(topic, session.id) - request.resolveDappInfoFromSession({peer: {metadata: {name: "Test DApp", url: "https://test.dapp", icons:[]}}}) verify(!!request, "expected request to be found") verify(request.isExpired(), "expected request to be expired") verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest") @@ -567,13 +579,11 @@ Item { handler.store.userAuthenticated(topic, session.id, "1234", "", message) verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest") sdk.sessionRequestUserAnswerResult(topic, session.id, false, "") - verify(displayToastMessageSpy.count === 1, "expected a toast message to be displayed") - compare(displayToastMessageSpy.signalArguments[0][0], "test.dapp sign request timed out") } function test_rejectExpiredSessionRequest() { - const sdk = handler.sdk + const sdk = handler.wcSdk const testAddressUpper = "0x3A" const chainId = 2 const method = "personal_sign" @@ -585,6 +595,7 @@ Item { verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past") + mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic) sdk.sessionRequestEvent(session) verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest") @@ -597,7 +608,7 @@ Item { function test_signFailedAuthOnExpiredRequest() { - const sdk = handler.sdk + const sdk = handler.wcSdk const testAddressUpper = "0x3A" const chainId = 2 const method = "personal_sign" @@ -609,6 +620,7 @@ Item { verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past") + mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic) sdk.sessionRequestEvent(session) verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest") @@ -621,14 +633,14 @@ Item { } TestCase { - id: walletConnectServiceTest - name: "WalletConnectService" + id: dappsServiceTest + name: "DAppsService" - property WalletConnectService service: null + property DAppsService service: null SignalSpy { id: connectDAppSpy - target: walletConnectServiceTest.service + target: dappsServiceTest.service signalName: "connectDApp" property var argPos: { @@ -640,35 +652,52 @@ Item { } } - readonly property SignalSpy sessionRequestSpy: SignalSpy { - target: walletConnectServiceTest.service - signalName: "sessionRequest" - - property var argPos: { - "id": 0 + function populateDAppData(topic) { + const dapp = { + topic, + name: Testing.dappName, + url: Testing.dappUrl, + iconUrl: Testing.dappFirstIcon, + connectorId: 0, + accountAddresses: [{address: "0x123"}], + rawSessions: [{session: {topic}}] } + findChild(service.dappsModule, "DAppsModel").model.append(dapp) } function init() { let walletStore = createTemporaryObject(walletStoreComponent, root) verify(!!walletStore) - let sdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab" }) + let sdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab", enabled: true }) verify(!!sdk) + let bcSdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab", enabled: false }) let store = createTemporaryObject(dappsStoreComponent, root) verify(!!store) - service = createTemporaryObject(serviceComponent, root, {wcSDK: sdk, store: store, walletRootStore: walletStore}) + let dappsModuleObj = createTemporaryObject(dappsModuleComponent, root, { + wcSdk: sdk, + bcSdk: bcSdk, + store: store, + accountsModel: walletStore.nonWatchAccounts, + networksModel: walletStore.filteredFlatModelWithOnlineStat + }) + + service = createTemporaryObject(serviceComponent, root, { + dappsModule: dappsModuleObj, + selectedAddress: "", + accountsModel: walletStore.nonWatchAccounts + }) verify(!!service) } function cleanup() { connectDAppSpy.clear() - sessionRequestSpy.clear() } function testSetupPair(sessionProposalPayload) { - let sdk = service.wcSDK - let walletStore = service.walletRootStore - let store = service.store + let sdk = service.dappsModule.wcSdk + let accountsModel = service.dappsModule.accountsModel + let networksModel = service.dappsModule.networksModel + let store = service.dappsModule.store service.pair("wc:12ab@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=12ab") compare(sdk.pairCalled, 1, "expected a call to sdk.pair") @@ -680,21 +709,21 @@ Item { // All calls to SDK are expected as events to be made by the wallet connect SDK let chainsForApproval = args.supportedNamespaces.eip155.chains - let networksArray = ModelUtils.modelToArray(walletStore.filteredFlatModel).map(entry => entry.chainId) + let networksArray = ModelUtils.modelToArray(networksModel).map(entry => entry.chainId) verify(networksArray.every(chainId => chainsForApproval.some(eip155Chain => eip155Chain === `eip155:${chainId}`)), "expect all the networks to be present") // We test here all accounts for one chain only, we have separate tests to validate that all accounts are present let allAccountsForApproval = args.supportedNamespaces.eip155.accounts - let accountsArray = ModelUtils.modelToArray(walletStore.nonWatchAccounts).map(entry => entry.address) + let accountsArray = ModelUtils.modelToArray(accountsModel).map(entry => entry.address) verify(accountsArray.every(address => allAccountsForApproval.some(eip155Address => eip155Address === `eip155:${networksArray[0]}:${address}`)), "expect at least all accounts for the first chain to be present" ) - return {sdk, walletStore, store, networksArray, accountsArray} + return {sdk, store, networksArray, accountsArray, networksModel, accountsModel} } function test_TestPairing() { - const {sdk, walletStore, store, networksArray, accountsArray} = testSetupPair(Testing.formatSessionProposal()) + const {sdk, store, networksArray, accountsArray, networksModel, accountsModel} = testSetupPair(Testing.formatSessionProposal()) compare(sdk.buildApprovedNamespacesCalls.length, 1, "expected a call to sdk.buildApprovedNamespaces") let allApprovedNamespaces = JSON.parse(Testing.formatBuildApprovedNamespacesResult(networksArray, accountsArray)) @@ -708,7 +737,7 @@ Item { compare(connectArgs[connectDAppSpy.argPos.dappIcon], Testing.dappFirstIcon, "expected dappIcon to be set") verify(!!connectArgs[connectDAppSpy.argPos.key], "expected key to be set") - let selectedAccount = walletStore.nonWatchAccounts.get(1).address + let selectedAccount = accountsModel.get(1).address service.approvePairSession(connectArgs[connectDAppSpy.argPos.key], connectArgs[connectDAppSpy.argPos.dappChains], selectedAccount) compare(sdk.buildApprovedNamespacesCalls.length, 2, "expected a call to sdk.buildApprovedNamespaces") const approvedArgs = sdk.buildApprovedNamespacesCalls[1] @@ -740,7 +769,7 @@ Item { } function test_TestPairingUnsupportedNetworks() { - const {sdk, walletStore, store} = testSetupPair(Testing.formatSessionProposal()) + const {sdk, store} = testSetupPair(Testing.formatSessionProposal()) const approvedArgs = sdk.buildApprovedNamespacesCalls[0] sdk.buildApprovedNamespacesResult(approvedArgs.id, {}, "Non conforming namespaces. approve() namespaces chains don't satisfy required namespaces") @@ -751,7 +780,7 @@ Item { function test_SessionRequestMainFlow() { // All calls to SDK are expected as events to be made by the wallet connect SDK - const sdk = service.wcSDK + const sdk = service.dappsModule.wcSdk const walletStore = service.walletRootStore const store = service.store @@ -762,17 +791,11 @@ Item { const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddress}"`] const topic = "b536a" const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic)) - // Expect to have calls to getActiveSessions from service initialization - const prevRequests = sdk.getActiveSessionsCallbacks.length + + populateDAppData(topic) sdk.sessionRequestEvent(session) - compare(sdk.getActiveSessionsCallbacks.length, prevRequests + 1, "expected DAppsRequestHandler call sdk.getActiveSessions") - const callback = sdk.getActiveSessionsCallbacks[prevRequests].callback - callback({"b536a": JSON.parse(Testing.formatApproveSessionResponse([chainId, 7], [testAddress]))}) - - compare(sessionRequestSpy.count, 1, "expected service.sessionRequest trigger") - const requestId = sessionRequestSpy.signalArguments[0][sessionRequestSpy.argPos.id] - const request = service.sessionRequestsModel.findById(requestId) + const request = service.sessionRequestsModel.findById(session.id) verify(!!request, "expected request to be found") compare(request.topic, topic, "expected topic to be set") compare(request.method, method, "expected method to be set") @@ -795,64 +818,6 @@ Item { // } } - Component { - id: dappsListProviderComponent - DAppsListProvider { - } - } - - TestCase { - name: "DAppsListProvider" - - property DAppsListProvider provider: null - - readonly property var dappsListReceivedJsonStr: '[{"url":"https://tst1.com","name":"name1","iconUrl":"https://tst1.com/u/1"},{"url":"https://tst2.com","name":"name2","iconUrl":"https://tst2.com/u/2"}]' - - function init() { - // Simulate the SDK not being ready - let sdk = createTemporaryObject(sdkComponent, root, {projectId: "12ab", sdkReady: false}) - verify(!!sdk) - let store = createTemporaryObject(dappsStoreComponent, root, { - dappsListReceivedJsonStr: dappsListReceivedJsonStr - }) - verify(!!store) - const walletStore = createTemporaryObject(walletStoreComponent, root) - verify(!!walletStore) - provider = createTemporaryObject(dappsListProviderComponent, root, {sdk: sdk, store: store, supportedAccountsModel: walletStore.nonWatchAccounts}) - verify(!!provider) - sdk.getActiveSessionsCallbacks = [] - } - - function cleanup() { - } - - // Implemented as a regression to metamask not having icons which failed dapps list - function test_TestUpdateDapps() { - // Validate that persistance fallback is working - compare(provider.dappsModel.count, 2, "expected dappsModel have the right number of elements") - let persistanceList = JSON.parse(dappsListReceivedJsonStr) - compare(provider.dappsModel.get(0).url, persistanceList[0].url, "expected url to be set") - compare(provider.dappsModel.get(0).iconUrl, persistanceList[0].iconUrl, "expected iconUrl to be set") - compare(provider.dappsModel.get(1).name, persistanceList[1].name, "expected name to be set") - - // Validate that SDK's `getActiveSessions` is not called if not ready - let sdk = provider.sdk - compare(sdk.getActiveSessionsCallbacks.length, 0, "expected no calls to sdk.getActiveSessions yet") - sdk.sdkReady = true - compare(sdk.getActiveSessionsCallbacks.length, 1, "expected a call to sdk.getActiveSessions when SDK becomes ready") - let callback = sdk.getActiveSessionsCallbacks[0].callback - const address = ModelUtils.get(provider.supportedAccountsModel, 0, "address") - let session = JSON.parse(Testing.formatApproveSessionResponse([1, 2], [address], {dappMetadataJsonString: Testing.noIconsDappMetadataJsonString})) - callback({"b536a": session, "b537b": session}) - compare(provider.dappsModel.count, 1, "expected dappsModel have the SDK's reported dapp, 2 sessions of the same dApp per 2 wallet account, meaning 1 dApp model entry") - compare(provider.dappsModel.get(0).iconUrl, "", "expected iconUrl to be missing") - let updateCalls = provider.store.updateWalletConnectSessionsCalls - compare(updateCalls.length, 1, "expected a call to store.updateWalletConnectSessions") - verify(updateCalls[0].activeTopicsJson.includes("b536a")) - verify(updateCalls[0].activeTopicsJson.includes("b537b")) - } - } - TestCase { name: "ServiceHelpers" diff --git a/storybook/qmlTests/tests/tst_SessionRequest.qml b/storybook/qmlTests/tests/tst_SessionRequest.qml new file mode 100644 index 0000000000..7d0be2d0e7 --- /dev/null +++ b/storybook/qmlTests/tests/tst_SessionRequest.qml @@ -0,0 +1,72 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import AppLayouts.Wallet.services.dapps.types 1.0 + +Item { + id: root + + TestCase { + id: sessionRequestTest + function test_parse_data() { + return [ + { + // valid eth_sign + expected: {"request":{"event":{"id":1730896224189361,"params":{"chainId":"eip155:1","request":{"method":"eth_sign","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896224189361,"method":"eth_sign","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":"\u0005=H\u0000Æ5NvÇ])\"È/µ³ôW{/àTòà"},"preparedData":"\u0005=H\u0000Æ5NvÇ])\"È/µ³ôW{/àTòà","value":"0","chainId":1},"error":0}, + event: {"id":1730896224189361,"params":{"chainId":"eip155:1","request":{"method":"eth_sign","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + }, + { + // valid personal_sign + expected: {"request":{"event":{"id":1730896110928724,"params":{"chainId":"eip155:1","request":{"method":"personal_sign","params":["0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765","0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","Example password"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896110928724,"method":"personal_sign","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":"Example `personal_sign` message"},"preparedData":"Example `personal_sign` message","value":"0","chainId":1},"error":0}, + event: {"id":1730896110928724,"params":{"chainId":"eip155:1","request":{"method":"personal_sign","params":["0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765","0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","Example password"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + }, + { + // valid typed_data + expected: {"request":{"event":{"id":1730896224189361,"params":{"chainId":"eip155:1","request":{"method":"eth_sign","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896224189361,"method":"eth_sign","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":`��\u0005=H\u0000�5NvǘZ�])\"�/���W{/����T��`},"preparedData":"��\u0005=H\u0000�5NvǘZ�])\"�/���W{/����T��","value":"0","chainId":1}, "error":0}, + event: {"id":1730896224189361,"params":{"chainId":"eip155:1","request":{"method":"eth_sign","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + }, + { + // valid eth_signTypedData_v4 + expected: {"request":{"event":{"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896495619543,"method":"eth_signTypedData_v4","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":"{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"},"preparedData":"{\n \"domain\": {\n \"chainId\": \"1\",\n \"name\": \"Ether Mail\",\n \"verifyingContract\": \"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\n \"version\": \"1\"\n },\n \"message\": {\n \"contents\": \"Hello, Bob!\",\n \"from\": {\n \"name\": \"Cow\",\n \"wallets\": [\n \"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\n \"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"\n ]\n },\n \"to\": [\n {\n \"name\": \"Bob\",\n \"wallets\": [\n \"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\n \"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\n \"0xB0B0b0b0b0b0B000000000000000000000000000\"\n ]\n }\n ],\n \"attachment\": \"0x\"\n },\n \"primaryType\": \"Mail\",\n \"types\": {\n \"EIP712Domain\": [\n {\n \"name\": \"name\",\n \"type\": \"string\"\n },\n {\n \"name\": \"version\",\n \"type\": \"string\"\n },\n {\n \"name\": \"chainId\",\n \"type\": \"uint256\"\n },\n {\n \"name\": \"verifyingContract\",\n \"type\": \"address\"\n }\n ],\n \"Group\": [\n {\n \"name\": \"name\",\n \"type\": \"string\"\n },\n {\n \"name\": \"members\",\n \"type\": \"Person[]\"\n }\n ],\n \"Mail\": [\n {\n \"name\": \"from\",\n \"type\": \"Person\"\n },\n {\n \"name\": \"to\",\n \"type\": \"Person[]\"\n },\n {\n \"name\": \"contents\",\n \"type\": \"string\"\n },\n {\n \"name\": \"attachment\",\n \"type\": \"bytes\"\n }\n ],\n \"Person\": [\n {\n \"name\": \"name\",\n \"type\": \"string\"\n },\n {\n \"name\": \"wallets\",\n \"type\": \"address[]\"\n }\n ]\n }\n}","value":"0","chainId":1},"error":0}, + event: {"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + }, + { + // valid sign permit + expected: {"request":{"event":{"id":1730896621650994,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Permit\":[{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"spender\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"nonce\",\"type\":\"uint256\"},{\"name\":\"deadline\",\"type\":\"uint256\"}]},\"primaryType\":\"Permit\",\"domain\":{\"name\":\"MyToken\",\"version\":\"1\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"chainId\":null},\"message\":{\"owner\":\"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\",\"spender\":\"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\",\"value\":3000,\"nonce\":0,\"deadline\":50000000000}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896621650994,"method":"eth_signTypedData_v4","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":"{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Permit\":[{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"spender\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"nonce\",\"type\":\"uint256\"},{\"name\":\"deadline\",\"type\":\"uint256\"}]},\"primaryType\":\"Permit\",\"domain\":{\"name\":\"MyToken\",\"version\":\"1\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"chainId\":null},\"message\":{\"owner\":\"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\",\"spender\":\"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\",\"value\":3000,\"nonce\":0,\"deadline\":50000000000}}"},"preparedData":"{\n \"types\": {\n \"EIP712Domain\": [\n {\n \"name\": \"name\",\n \"type\": \"string\"\n },\n {\n \"name\": \"version\",\n \"type\": \"string\"\n },\n {\n \"name\": \"chainId\",\n \"type\": \"uint256\"\n },\n {\n \"name\": \"verifyingContract\",\n \"type\": \"address\"\n }\n ],\n \"Permit\": [\n {\n \"name\": \"owner\",\n \"type\": \"address\"\n },\n {\n \"name\": \"spender\",\n \"type\": \"address\"\n },\n {\n \"name\": \"value\",\n \"type\": \"uint256\"\n },\n {\n \"name\": \"nonce\",\n \"type\": \"uint256\"\n },\n {\n \"name\": \"deadline\",\n \"type\": \"uint256\"\n }\n ]\n },\n \"primaryType\": \"Permit\",\n \"domain\": {\n \"name\": \"MyToken\",\n \"version\": \"1\",\n \"verifyingContract\": \"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\n \"chainId\": null\n },\n \"message\": {\n \"owner\": \"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\",\n \"spender\": \"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\",\n \"value\": 3000,\n \"nonce\": 0,\n \"deadline\": 50000000000\n }\n}","value":"0","chainId":1},"error":0}, + event: {"id":1730896621650994,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Permit\":[{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"spender\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"nonce\",\"type\":\"uint256\"},{\"name\":\"deadline\",\"type\":\"uint256\"}]},\"primaryType\":\"Permit\",\"domain\":{\"name\":\"MyToken\",\"version\":\"1\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"chainId\":null},\"message\":{\"owner\":\"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\",\"spender\":\"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\",\"value\":3000,\"nonce\":0,\"deadline\":50000000000}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + }, + { + // valid SIWE + expected: {"request":{"event":{"id":1730896696709302,"params":{"chainId":"eip155:1","request":{"method":"personal_sign","params":["0x6d6574616d61736b2e6769746875622e696f2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078374634374332653138613442426635343837453666623038326543324439416230453664373234300a0a492061636365707420746865204d6574614d61736b205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a2068747470733a2f2f6d6574616d61736b2e6769746875622e696f0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2033323839313735370a4973737565642041743a20323032312d30392d33305431363a32353a32342e3030305a","0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","Example password"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896696709302,"method":"personal_sign","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":"metamask.github.io wants you to sign in with your Ethereum account:\n0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\n\nI accept the MetaMask Terms of Service: https://community.metamask.io/tos\n\nURI: https://metamask.github.io\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z"},"preparedData":"metamask.github.io wants you to sign in with your Ethereum account:\n0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\n\nI accept the MetaMask Terms of Service: https://community.metamask.io/tos\n\nURI: https://metamask.github.io\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z","value":"0","chainId":1},"error":0}, + event: {"id":1730896696709302,"params":{"chainId":"eip155:1","request":{"method":"personal_sign","params":["0x6d6574616d61736b2e6769746875622e696f2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078374634374332653138613442426635343837453666623038326543324439416230453664373234300a0a492061636365707420746865204d6574614d61736b205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a2068747470733a2f2f6d6574616d61736b2e6769746875622e696f0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2033323839313735370a4973737565642041743a20323032312d30392d33305431363a32353a32342e3030305a","0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","Example password"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + }, + { + // valid EIP 1159 transaction + expected: {"request":{"event":{"id":1730899979094571,"params":{"chainId":"eip155:1","request":{"method":"eth_sendTransaction","params":[{"from":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","gasLimit":"0x5028","maxFeePerGas":"0x2540be400","maxPriorityFeePerGas":"0x3b9aca00","to":"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb","value":"0x0"}]}},"topic":"147fa9ddffcf9d782b5e002eb36b041e43a1db2bad79a598da6926105ce6680f","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"147fa9ddffcf9d782b5e002eb36b041e43a1db2bad79a598da6926105ce6680f","requestId":1730899979094571,"method":"eth_sendTransaction","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"tx":{"from":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","gasLimit":"0x5028","maxFeePerGas":"0x2540be400","maxPriorityFeePerGas":"0x3b9aca00","to":"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb","value":"0x0"}},"preparedData":"{\n \"from\": \"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\",\n \"gasLimit\": 20520,\n \"maxFeePerGas\": \"10\",\n \"maxPriorityFeePerGas\": \"1\",\n \"to\": \"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb\",\n \"value\": \"0\"\n}","value":"0","transaction":{"from":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","gasLimit":"0x5028","maxFeePerGas":"0x2540be400","maxPriorityFeePerGas":"0x3b9aca00","to":"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb","value":"0x0"},"chainId":1},"error":0}, + event: {"id":1730899979094571,"params":{"chainId":"eip155:1","request":{"method":"eth_sendTransaction","params":[{"from":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","gasLimit":"0x5028","maxFeePerGas":"0x2540be400","maxPriorityFeePerGas":"0x3b9aca00","to":"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb","value":"0x0"}]}},"topic":"147fa9ddffcf9d782b5e002eb36b041e43a1db2bad79a598da6926105ce6680f","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + } + ] + } + function test_parse(data) { + const { expected, event } = data + const result = SessionRequest.parse(event) + + compare(result.error, expected.error) + compare(result.request.topic, expected.request.topic) + compare(result.request.requestId, expected.request.requestId) + compare(result.request.method, expected.request.method) + compare(result.request.account, expected.request.account) + compare(result.request.value, expected.request.value) + compare(result.request.chainId, expected.request.chainId) + compare(!!result.request.transaction, !!expected.request.transaction) + if (!!result.request.transaction) { + compare(result.request.transaction.from, expected.request.transaction.from) + compare(result.request.transaction.gasLimit, expected.request.transaction.gasLimit) + compare(result.request.transaction.maxFeePerGas, expected.request.transaction.maxFeePerGas) + compare(result.request.transaction.maxPriorityFeePerGas, expected.request.transaction.maxPriorityFeePerGas) + compare(result.request.transaction.to, expected.request.transaction.to) + compare(result.request.transaction.value, expected.request.transaction.value) + } + } + } +} \ No newline at end of file diff --git a/storybook/qmlTests/tests/tst_SessionRequestWithAuth.qml b/storybook/qmlTests/tests/tst_SessionRequestWithAuth.qml index 1bc78cf260..917d5cece2 100644 --- a/storybook/qmlTests/tests/tst_SessionRequestWithAuth.qml +++ b/storybook/qmlTests/tests/tst_SessionRequestWithAuth.qml @@ -47,6 +47,9 @@ Item { sourceId: 0 data: "data" preparedData: "preparedData" + dappUrl: "dappUrl" + dappIcon: "dappIcon" + dappName: "dappName" } } @@ -88,7 +91,6 @@ Item { componentUnderTest.store.userAuthenticationFailed("topic", "id") compare(componentUnderTest.executeSpy.count, 0) - compare(componentUnderTest.rejectedSpy.count, 1) compare(componentUnderTest.authFailedSpy.count, 1) compare(componentUnderTest.store.authenticateUserCalls.length, 1) } diff --git a/storybook/qmlTests/tests/tst_SignRequestPlugin.qml b/storybook/qmlTests/tests/tst_SignRequestPlugin.qml new file mode 100644 index 0000000000..6afa2e4752 --- /dev/null +++ b/storybook/qmlTests/tests/tst_SignRequestPlugin.qml @@ -0,0 +1,268 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import AppLayouts.Wallet.services.dapps 1.0 +import AppLayouts.Wallet.services.dapps.plugins 1.0 +import AppLayouts.Wallet.services.dapps.types 1.0 + +import shared.stores 1.0 +import utils 1.0 + +Item { + id: root + + Component { + id: signRequestPluginComponent + SignRequestPlugin { + id: plugin + property SignalSpy acceptedSpy: SignalSpy { target: plugin; signalName: "accepted" } + property SignalSpy rejectedSpy: SignalSpy { target: plugin; signalName: "rejected" } + property SignalSpy signCompletedSpy: SignalSpy { target: plugin; signalName: "signCompleted" } + + sdk: WalletConnectSDKBase { + id: sdk + enabled: true + projectId: "" + property bool sdkReady: true + + property var getActiveSessionsCallbacks: [] + getActiveSessions: function(callback) { + getActiveSessionsCallbacks.push({callback}) + } + + property var acceptSessionRequestCalls: [] + acceptSessionRequest: function(topic, id, signature) { + acceptSessionRequestCalls.push({topic, id, signature}) + } + + property var rejectSessionRequestCalls: [] + rejectSessionRequest: function(topic, id, error) { + rejectSessionRequestCalls.push({topic, id, error}) + } + } + store: DAppsStore { + id: dappsStore + + signal userAuthenticated(string topic, string id, string password, string pin, string payload) + signal userAuthenticationFailed(string topic, string id) + signal signingResult(string topic, string id, string data) + + function hexToDec(hex) { + return parseInt(hex, 16) + } + + function getEstimatedTime() { + return Constants.TransactionEstimatedTime.LessThanThreeMins + } + + function convertFeesInfoToHex(feesInfo) { + return null + } + + property var mockedSuggestedFees: ({ + gasPrice: 2.0, + baseFee: 5.0, + maxPriorityFeePerGas: 2.0, + maxFeePerGasL: 1.0, + maxFeePerGasM: 1.1, + maxFeePerGasH: 1.2, + l1GasFee: 0.0, + eip1559Enabled: true + }) + function getSuggestedFees() { + return mockedSuggestedFees + } + + property var authenticateUserCalls: [] + function authenticateUser(topic, id, address) { + authenticateUserCalls.push({topic, id, address}) + } + + property var signMessageCalls: [] + function signMessage(topic, id, address, message, password, pin) { + signMessageCalls.push({topic, id, address, password, pin}) + } + + property var signMessageUnsafeCalls: [] + function signMessageUnsafe(topic, id, address, data, password, pin) { + signMessageUnsafeCalls.push({topic, id, address, data, password, pin}) + } + + property var safeSignTypedDataCalls: [] + function safeSignTypedData(topic, id, address, message, chainId, legacy, password, pin) { + safeSignTypedDataCalls.push({topic, id, address, message, chainId, legacy, password, pin}) + } + + property var signTransactionCalls: [] + function signTransaction(topic, id, address, chainId, txObj, password, pin) { + signTransactionCalls.push({topic, id, address, chainId, txObj, password, pin}) + } + + property var sendTransactionCalls: [] + function sendTransaction(topic, id, address, chainID, txObj, password, pin) { + sendTransactionCalls.push({topic, id, address, chainID, txObj, password, pin}) + } + } + dappsModel: ListModel { + id: dappsModel + } + groupedAccountAssetsModel: ListModel { + id: groupedAccountAssetsModel + } + networksModel: ListModel { + id: networksModel + ListElement { + chainId: 1 + layer: 1 + } + } + accountsModel: ListModel { + id: accountsModel + ListElement { + address: "0x123" + } + } + currentCurrency: "USD" + requests: SessionRequestsModel {} + getFiatValue: (balance, cryptoSymbol) => { + return parseFloat(balance) + } + } + } + + TestCase { + id: signRequestPluginTest + + property SignRequestPlugin componentUnderTest: null + + function populateDAppData(topic) { + const dapp = { + topic, + name: "Example", + url: "https://example.com", + iconUrl: "https://example.com/icon.png", + connectorId: 0, + accountAddresses: [{address: "0x123"}], + rawSessions: [{session: {topic}}] + } + componentUnderTest.dappsModel.append(dapp) + } + + function executeRequest(signEvent) { + populateDAppData(signEvent.topic) + componentUnderTest.sdk.sessionRequestEvent(signEvent) + // Execute the request + const request = componentUnderTest.requests.get(0) + request.requestItem.execute("passowrd", "pin") + + componentUnderTest.store.signingResult(signEvent.topic, signEvent.id, "result") + compare(componentUnderTest.sdk.acceptSessionRequestCalls.length, 1, "Accept session request should be called") + compare(componentUnderTest.sdk.acceptSessionRequestCalls[0].signature, "result", "Accept session request should be called with the correct signature") + compare(componentUnderTest.sdk.acceptSessionRequestCalls[0].topic.toString(), signEvent.topic.toString(), "Accept session request should be called with the correct topic") + compare(componentUnderTest.sdk.acceptSessionRequestCalls[0].id.toString(), signEvent.id.toString(), "Accept session request should be called with the correct id") + + compare(componentUnderTest.acceptedSpy.count, 1, "Accepted signal should be emitted") + } + + function init() { + componentUnderTest = createTemporaryObject(signRequestPluginComponent, root) + } + + function test_signMessage() { + const signEvent = {"id":1730896110928724,"params":{"chainId":"eip155:1","request":{"method":"personal_sign","params":["0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765","0x123","Example password"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + // Case 1: DApp not found + ignoreWarning(`Error finding dapp for topic ${signEvent.topic} id ${signEvent.id}`) + componentUnderTest.sdk.sessionRequestEvent(signEvent) + compare(componentUnderTest.sdk.rejectSessionRequestCalls.length, 1, "Reject session request should not be called") + + // Case 2: DApp found + executeRequest(signEvent) + compare(componentUnderTest.store.signMessageCalls.length, 1, "Sign message should be called") + componentUnderTest.sdk.sessionRequestUserAnswerResult(signEvent.topic, signEvent.id, true, null) + + const requestItem = componentUnderTest.requests.get(0).requestItem + compare(requestItem.requestId, signEvent.id.toString(), "Request id should be set") + compare(requestItem.topic, signEvent.topic.toString(), "Topic should be set") + compare(requestItem.method, signEvent.params.request.method, "Method should be set") + compare(requestItem.accountAddress, signEvent.params.request.params[1], "Account address should be set") + compare(requestItem.chainId, signEvent.params.chainId.split(':').pop().trim(), "Chain id should be set") + compare(requestItem.sourceId, componentUnderTest.dappsModel.get(0).connectorId, "Source id should be set") + + compare(componentUnderTest.signCompletedSpy.count, 1, "Sign completed signal should be emitted") + } + + function test_signMessageUnsafe() { + const signEvent = {"id":1730896224189361,"params":{"chainId":"eip155:1","request":{"method":"eth_sign","params":["0x123","0x123"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + + executeRequest(signEvent) + + compare(componentUnderTest.store.signMessageUnsafeCalls.length, 1, "Sign message unsafe should be called") + componentUnderTest.sdk.sessionRequestUserAnswerResult(signEvent.topic, signEvent.id, true, null) + + compare(componentUnderTest.signCompletedSpy.count, 1, "Sign completed signal should be emitted") + } + + function test_safeSignTypedData() { + const signEvent = {"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x123","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + + executeRequest(signEvent) + + compare(componentUnderTest.store.safeSignTypedDataCalls.length, 1, "Safe sign typed data should be called") + componentUnderTest.sdk.sessionRequestUserAnswerResult(signEvent.topic, signEvent.id, true, null) + + compare(componentUnderTest.signCompletedSpy.count, 1, "Sign completed signal should be emitted") + } + + function test_sendTransaction() { + const signEvent = {"id":1730899979094571,"params":{"chainId":"eip155:1","request":{"method":"eth_sendTransaction","params":[{"from":"0x123","gasLimit":"0x5028","maxFeePerGas":"0x2540be400","maxPriorityFeePerGas":"0x3b9aca00","to":"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb","value":"0x0"}]}},"topic":"147fa9ddffcf9d782b5e002eb36b041e43a1db2bad79a598da6926105ce6680f","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + + executeRequest(signEvent) + + compare(componentUnderTest.store.sendTransactionCalls.length, 1, "Send transaction should be called") + componentUnderTest.sdk.sessionRequestUserAnswerResult(signEvent.topic, signEvent.id, true, null) + + compare(componentUnderTest.signCompletedSpy.count, 1, "Sign completed signal should be emitted") + } + + function reject_sign() { + const signEvent = {"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x123","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + populateDAppData(signEvent.topic) + componentUnderTest.sdk.sessionRequestEvent(signEvent) + // Execute the request + const request = componentUnderTest.requests.get(0) + request.requestItem.rejected(false) + + compare(componentUnderTest.sdk.rejectSessionRequestCalls.length, 1, "Reject session request should be called") + compare(componentUnderTest.rejectedSpy.count, 1, "Rejected signal should be emitted") + compare(componentUnderTest.signCompletedSpy.count, 0, "Sign completed signal should not be emitted") + } + + function test_authFailed() { + const signEvent = {"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x123","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + populateDAppData(signEvent.topic) + componentUnderTest.sdk.sessionRequestEvent(signEvent) + // Execute the request + const request = componentUnderTest.requests.get(0) + + request.requestItem.authFailed() + compare(componentUnderTest.sdk.rejectSessionRequestCalls.length, 1, "Reject session request should be called") + compare(componentUnderTest.rejectedSpy.count, 1, "Rejected signal should be emitted") + compare(componentUnderTest.signCompletedSpy.count, 0, "Sign completed signal should not be emitted") + } + + function test_signMessageFails() { + const signEvent = {"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x123","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}} + + populateDAppData(signEvent.topic) + componentUnderTest.sdk.sessionRequestEvent(signEvent) + // Execute the request + const request = componentUnderTest.requests.get(0) + request.requestItem.execute("passowrd", "pin") + + componentUnderTest.store.signingResult(signEvent.topic, signEvent.id, "") + compare(componentUnderTest.sdk.rejectSessionRequestCalls.length, 1, "Reject session request should be called") + compare(componentUnderTest.rejectedSpy.count, 1, "Rejected signal should be emitted") + compare(componentUnderTest.signCompletedSpy.count, 0, "Sign completed signal should not be emitted") + } + } +} \ No newline at end of file diff --git a/storybook/qmlTests/tests/tst_WCDAppsProvider.qml b/storybook/qmlTests/tests/tst_WCDAppsProvider.qml new file mode 100644 index 0000000000..92b6f28f1c --- /dev/null +++ b/storybook/qmlTests/tests/tst_WCDAppsProvider.qml @@ -0,0 +1,133 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import AppLayouts.Wallet.services.dapps 1.0 +import shared.stores 1.0 + +Item { + id: root + + Component { + id: wcDAppsProviderComponent + WCDappsProvider { + id: wcDAppsProvider + readonly property SignalSpy connectedSpy: SignalSpy { target: wcDAppsProvider; signalName: "connected" } + readonly property SignalSpy disconnectedSpy: SignalSpy { target: wcDAppsProvider; signalName: "disconnected" } + supportedAccountsModel: ListModel { + ListElement { + address: "0x123" + } + } + store: DAppsStore { + signal dappsListReceived(string dappsJson) + + property var addWalletConnectSessionCalls: [] + function addWalletConnectSession(sessionJson) { + addWalletConnectSessionCalls.push(sessionJson) + } + + property var deactivateWalletConnectSessionCalls: [] + function deactivateWalletConnectSession(topic) { + deactivateWalletConnectSessionCalls.push(topic) + } + + property var updateWalletConnectSessionsCalls: [] + function updateWalletConnectSessions(topics) { + updateWalletConnectSessionsCalls.push(topics) + } + + property var getDAppsCalls: [] + function getDapps() { + getDAppsCalls.push(true) + return [] + } + } + sdk: WalletConnectSDKBase { + id: sdk + enabled: true + projectId: "" + property bool sdkReady: true + property var activeSessions: ({}) + getActiveSessions: function(callback) { + callback(sdk.activeSessions) + } + } + } + } + + function buildSession(dappUrl, dappName, dappIcon, proposalId, account, chains) { + let sessionTemplate = (dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains) => { + return { + peer: { + metadata: { + description: "-", + icons: [ + dappIcon + ], + name: dappName, + url: dappUrl + } + }, + namespaces: { + eip155: { + accounts: [eipAccount], + chains: eipChains + } + }, + pairingTopic: proposalId, + topic: dappUrl + }; + } + + const eipAccount = account ? `eip155:${account}` : "" + const eipChains = chains ? chains.map((chain) => `eip155:${chain}`) : [] + + return sessionTemplate(dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains) + } + + TestCase { + id: wcDAppsProviderTest + + property WCDappsProvider componentUnderTest: null + function init() { + componentUnderTest = createTemporaryObject(wcDAppsProviderComponent, root) + } + + function test_addRemoveSession() { + const newSession = buildSession("https://example.com", "Example", "https://example.com/icon.png", "123", "0x123", ["1"]) + componentUnderTest.sdk.activeSessions["https://example.com"] = newSession + componentUnderTest.sdk.approveSessionResult("requestID", newSession, null) + + compare(componentUnderTest.store.addWalletConnectSessionCalls.length, 1, "addWalletConnectSession should be called once") + compare(componentUnderTest.connectedSpy.count, 1, "Connected signal should be emitted once") + compare(componentUnderTest.connectedSpy.signalArguments[0][0], "requestID", "Connected signal should have correct proposalId") + compare(componentUnderTest.connectedSpy.signalArguments[0][1], "https://example.com", "Connected signal should have correct topic") + compare(componentUnderTest.connectedSpy.signalArguments[0][2], "https://example.com", "Connected signal should have correct dAppUrl") + + const dapp = componentUnderTest.getByTopic("https://example.com") + verify(!!dapp, "DApp should be found") + compare(dapp.name, "Example", "DApp should have correct name") + compare(dapp.url, "https://example.com", "DApp should have correct url") + compare(dapp.iconUrl, "https://example.com/icon.png", "DApp should have correct iconUrl") + compare(dapp.topic, "https://example.com", "DApp should have correct topic") + compare(dapp.connectorId, componentUnderTest.connectorId, "DApp should have correct connectorId") + compare(dapp.accountAddresses.count, 1, "DApp should have correct accountAddresses count") + compare(dapp.accountAddresses.get(0).address, "0x123", "DApp should have correct accountAddresses address") + compare(dapp.rawSessions.count, 1, "DApp should have correct rawSessions count") + + componentUnderTest.sdk.sessionDelete("https://example.com", "") + compare(componentUnderTest.store.deactivateWalletConnectSessionCalls.length, 1, "deactivateWalletConnectSession should be called once") + compare(componentUnderTest.disconnectedSpy.count, 1, "Disconnected signal should be emitted once") + compare(componentUnderTest.disconnectedSpy.signalArguments[0][0], "https://example.com", "Disconnected signal should have correct topic") + compare(componentUnderTest.disconnectedSpy.signalArguments[0][1], "https://example.com", "Disconnected signal should have correct dAppUrl") + } + + function test_disabledSDK() { + componentUnderTest.sdk.enabled = false + componentUnderTest.sdk.approveSessionResult("requestID", buildSession("https://example.com", "Example", "https://example.com/icon.png", "123", "0x123", ["1"]), "") + compare(componentUnderTest.connectedSpy.count, 0, "Connected signal should not be emitted") + componentUnderTest.sdk.sessionDelete("https://example.com", "") + compare(componentUnderTest.disconnectedSpy.count, 0, "Disconnected signal should not be emitted") + } + } +} \ No newline at end of file diff --git a/storybook/stubs/shared/stores/BrowserConnectStore.qml b/storybook/stubs/shared/stores/BrowserConnectStore.qml new file mode 100644 index 0000000000..2b87551bfc --- /dev/null +++ b/storybook/stubs/shared/stores/BrowserConnectStore.qml @@ -0,0 +1,4 @@ +import QtQuick 2.15 + +QtObject { +} \ No newline at end of file diff --git a/storybook/stubs/shared/stores/qmldir b/storybook/stubs/shared/stores/qmldir index d7a633428c..73ce556341 100644 --- a/storybook/stubs/shared/stores/qmldir +++ b/storybook/stubs/shared/stores/qmldir @@ -8,3 +8,4 @@ PermissionsStore 1.0 PermissionsStore.qml ProfileStore 1.0 ProfileStore.qml RootStore 1.0 RootStore.qml UtilsStore 1.0 UtilsStore.qml +BrowserConnectStore 1.0 BrowserConnectStore.qml diff --git a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml index 24253bc623..5d9a329efa 100644 --- a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml +++ b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml @@ -69,12 +69,11 @@ DappsComboBox { signal disconnectRequested(string connectionId) signal pairingRequested(string uri) signal pairingValidationRequested(string uri) - signal connectionAccepted(var pairingId, var chainIds, string selectedAccount) - signal connectionDeclined(var pairingId) + signal connectionAccepted(string pairingId, var chainIds, string selectedAccount) + signal connectionDeclined(string pairingId) signal signRequestAccepted(string connectionId, string requestId) signal signRequestRejected(string connectionId, string requestId) - - signal subscribeForFeeUpdates(string connectionId, string requestId) + signal signRequestIsLive(string connectionId, string requestId) /// Response to pairingValidationRequested function pairingValidated(validationState) { @@ -349,7 +348,7 @@ DappsComboBox { hasExpiryDate: !!request.expirationTimestamp onOpened: { - root.subscribeForFeeUpdates(request.topic, request.requestId) + root.signRequestIsLive(request.topic, request.requestId) } onClosed: { diff --git a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml index 2aa3dba800..f91a92e16c 100644 --- a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml +++ b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml @@ -137,38 +137,38 @@ Item { id: dappsWorkflow Layout.alignment: Qt.AlignTop - readonly property WalletConnectService wcService: Global.walletConnectService + readonly property DAppsService dAppsService: Global.dAppsService spacing: 8 visible: !root.walletStore.showSavedAddresses - && (wcService.walletConnectFeatureEnabled || wcService.connectorFeatureEnabled) - && wcService.serviceAvailableToCurrentAddress - enabled: !!wcService && wcService.isServiceOnline + && (dAppsService.walletConnectFeatureEnabled || dAppsService.connectorFeatureEnabled) + && dAppsService.serviceAvailableToCurrentAddress + enabled: !!dAppsService && dAppsService.isServiceOnline - walletConnectEnabled: wcService.walletConnectFeatureEnabled - connectorEnabled: wcService.connectorFeatureEnabled + walletConnectEnabled: dAppsService.walletConnectFeatureEnabled + connectorEnabled: dAppsService.connectorFeatureEnabled loginType: root.loginType selectedAccountAddress: root.walletStore.selectedAddress - model: wcService.dappsModel - accountsModel: wcService.validAccounts - networksModel: wcService.flatNetworks - sessionRequestsModel: wcService.sessionRequestsModel + model: dAppsService.dappsModel + accountsModel: root.walletStore.nonWatchAccounts + networksModel: root.walletStore.filteredFlatModel + sessionRequestsModel: dAppsService.sessionRequestsModel - formatBigNumber: (number, symbol, noSymbolOption) => wcService.walletRootStore.currencyStore.formatBigNumber(number, symbol, noSymbolOption) + formatBigNumber: (number, symbol, noSymbolOption) => root.walletStore.currencyStore.formatBigNumber(number, symbol, noSymbolOption) - onDisconnectRequested: (connectionId) => wcService.disconnectDapp(connectionId) - onPairingRequested: (uri) => wcService.pair(uri) - onPairingValidationRequested: (uri) => wcService.validatePairingUri(uri) - onConnectionAccepted: (pairingId, chainIds, selectedAccount) => wcService.approvePairSession(pairingId, chainIds, selectedAccount) - onConnectionDeclined: (pairingId) => wcService.rejectPairSession(pairingId) - onSignRequestAccepted: (connectionId, requestId) => wcService.sign(connectionId, requestId) - onSignRequestRejected: (connectionId, requestId) => wcService.rejectSign(connectionId, requestId, false /*hasError*/) - onSubscribeForFeeUpdates: (connectionId, requestId) => wcService.subscribeForFeeUpdates(connectionId, requestId) + onDisconnectRequested: (connectionId) => dAppsService.disconnectDapp(connectionId) + onPairingRequested: (uri) => dAppsService.pair(uri) + onPairingValidationRequested: (uri) => dAppsService.validatePairingUri(uri) + onConnectionAccepted: (pairingId, chainIds, selectedAccount) => dAppsService.approvePairSession(pairingId, chainIds, selectedAccount) + onConnectionDeclined: (pairingId) => dAppsService.rejectPairSession(pairingId) + onSignRequestAccepted: (connectionId, requestId) => dAppsService.sign(connectionId, requestId) + onSignRequestRejected: (connectionId, requestId) => dAppsService.rejectSign(connectionId, requestId, false /*hasError*/) + onSignRequestIsLive: (connectionId, requestId) => dAppsService.signRequestIsLive(connectionId, requestId) Connections { - target: dappsWorkflow.wcService + target: dappsWorkflow.dAppsService function onPairingValidated(validationState) { dappsWorkflow.pairingValidated(validationState) } diff --git a/ui/app/AppLayouts/Wallet/services/dapps/BCDappsProvider.qml b/ui/app/AppLayouts/Wallet/services/dapps/BCDappsProvider.qml index 66419d4c6f..cbfaad6f08 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/BCDappsProvider.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/BCDappsProvider.qml @@ -9,64 +9,86 @@ import utils 1.0 DAppsModel { id: root - required property BrowserConnectStore store + required property WalletConnectSDKBase bcSDK readonly property int connectorId: Constants.StatusConnect - property bool enabled: true + readonly property bool enabled: bcSDK.enabled + + signal connected(string pairingId, string topic, string dAppUrl) + signal disconnected(string topic, string dAppUrl) Connections { - target: root.store + target: root.bcSDK enabled: root.enabled - function onConnected(dappJson) { - const dapp = JSON.parse(dappJson) - const { url, name, icon, sharedAccount } = dapp - - if (!url) { - console.warn(invalidDAppUrlError) + function onSessionDelete(topic, err) { + const dapp = root.getByTopic(topic) + if (!dapp) { + console.warn("DApp not found for topic - cannot delete session", topic) return } - root.append({ - name, - url, - iconUrl: icon, - topic: url, - connectorId: root.connectorId, - accountAddresses: [{address: sharedAccount}], - rawSessions: [dapp] - }) + + root.remove(topic) + root.disconnected(topic, dapp.url) } - function onDisconnected(dappJson) { - const dapp = JSON.parse(dappJson) - const { url } = dapp - - if (!url) { - console.warn(invalidDAppUrlError) + function onApproveSessionResult(proposalId, session, error) { + if (error) { + console.warn("Failed to approve session", error) return } - root.remove(dapp.url) + + const dapp = d.sessionToDApp(session) + root.append(dapp) + root.connected(proposalId, dapp.topic, dapp.url) + } + } + + QtObject { + id: d + function sessionToDApp(session) { + const dapp = session.peer.metadata + if (!!dapp.icons && dapp.icons.length > 0) { + dapp.iconUrl = dapp.icons[0] + } else { + dapp.iconUrl = "" + } + const accounts = DAppsHelpers.getAccountsInSession(session) + dapp.accountAddresses = accounts.map(account => ({address: account})) + dapp.topic = session.topic + dapp.rawSessions = [session] + dapp.connectorId = root.connectorId + return dapp + } + function getPersistedDapps() { + if (!root.enabled) { + return [] + } + let dapps = [] + root.bcSDK.getActiveSessions((allSessions) => { + if (!allSessions) { + return + } + + for (const sessionID in allSessions) { + const session = allSessions[sessionID] + const dapp = sessionToDApp(session) + dapps.push(dapp) + } + }) + return dapps + } + + function resetModel() { + root.clear() + const dapps = d.getPersistedDapps() + for (let i = 0; i < dapps.length; i++) { + root.append(dapps[i]) + } } } Component.onCompleted: { - if (root.enabled) { - const dappsStr = root.store.getDApps() - if (dappsStr) { - const dapps = JSON.parse(dappsStr) - dapps.forEach(dapp => { - const { url, name, iconUrl, sharedAccount } = dapp - root.append({ - name, - url, - iconUrl, - topic: url, - connectorId: root.connectorId, - accountAddresses: [{address: sharedAccount}], - rawSessions: [dapp] - }) - }) - } - } + d.resetModel() } } diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsModel.qml b/ui/app/AppLayouts/Wallet/services/dapps/DAppsModel.qml index 741b477510..b4eb653298 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/DAppsModel.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/DAppsModel.qml @@ -4,6 +4,7 @@ import StatusQ.Core.Utils 0.1 QObject { id: root + objectName: "DAppsModel" // RoleNames // name: string // url: string @@ -15,14 +16,22 @@ QObject { // rawSessions: [{session: object}] readonly property ListModel model: ListModel {} + // Appending a new DApp to the model + // Required properties: url, topic, connectorId, accountAddresses + // Optional properties: name, iconUrl, chains, rawSessions function append(dapp) { try { - const {name, url, iconUrl, topic, accountAddresses, connectorId, rawSessions } = dapp - if (!name || !url || !iconUrl || !topic || !connectorId || !accountAddresses || !rawSessions) { + let {name, url, iconUrl, topic, accountAddresses, connectorId, rawSessions } = dapp + if (!url || !topic || !connectorId || !accountAddresses) { console.warn("DAppsModel - Failed to append dapp, missing required fields", JSON.stringify(dapp)) return } + name = name || "" + iconUrl = iconUrl || "" + accountAddresses = accountAddresses || [] + rawSessions = rawSessions || [] + root.model.append({ name, url, @@ -38,12 +47,21 @@ QObject { } function remove(topic) { - for (let i = 0; i < root.model.count; i++) { - const dapp = root.model.get(i) - if (dapp.topic == topic) { - root.model.remove(i) - break - } + const { dapp, index, sessionIndex } = findDapp(topic) + if (!dapp) { + console.warn("DAppsModel - Failed to remove dapp, not found", topic) + return + } + + if (dapp.rawSessions.count === 1) { + root.model.remove(index) + return + } + + const rawSession = dapp.rawSessions.get(sessionIndex) + dapp.rawSessions.remove(sessionIndex) + if (rawSession.topic == dapp.topic) { + root.model.setProperty(index, "topic", dapp.rawSessions.get(0).topic) } } @@ -52,20 +70,36 @@ QObject { } function getByTopic(topic) { + const dappTemplate = (dapp) => { + return { + name: dapp.name, + url: dapp.url, + iconUrl: dapp.iconUrl, + topic: dapp.topic, + connectorId: dapp.connectorId, + accountAddresses: dapp.accountAddresses, + rawSessions: dapp.rawSessions + } + } + + const { dapp } = findDapp(topic) + if (!dapp) { + return null + } + return dappTemplate(dapp) + } + + function findDapp(topic) { for (let i = 0; i < root.model.count; i++) { const dapp = root.model.get(i) - if (dapp.topic == topic) { - return { - name: dapp.name, - url: dapp.url, - iconUrl: dapp.iconUrl, - topic: dapp.topic, - connectorId: dapp.connectorId, - accountAddresses: dapp.accountAddresses, - rawSessions: dapp.rawSessions + for (let j = 0; j < dapp.rawSessions.count; j++) { + if (dapp.rawSessions.get(j).topic == topic) { + return { dapp, index: i, sessionIndex: j } + break } } } - return null + + return { dapp: null, index: -1, sessionIndex: -1 } } } \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsModule.qml b/ui/app/AppLayouts/Wallet/services/dapps/DAppsModule.qml new file mode 100644 index 0000000000..213a8b893e --- /dev/null +++ b/ui/app/AppLayouts/Wallet/services/dapps/DAppsModule.qml @@ -0,0 +1,309 @@ +import QtQuick 2.15 + +import AppLayouts.Wallet.services.dapps 1.0 +import AppLayouts.Wallet.services.dapps.plugins 1.0 +import AppLayouts.Wallet.services.dapps.types 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStore + +import StatusQ.Core.Utils 0.1 as SQUtils + +import shared.stores 1.0 +import utils 1.0 + +/// Component that provides the dapps integration for the wallet. +/// It provides the following features: +/// - WalletConnect integration +/// - WalletConnect pairing +/// - WalletConnect sessions management +/// - WalletConnect signing requests +/// - WalletConnect SIWE +/// - WalletConnect online status +/// - BrowserConnect integration +/// - BrowserConnect pairing +/// - BrowserConnect - access to persistent sessions +/// - BrowserConnect - access to persistent signing requests +/// - BrowserConnect signing requests +/// - BrowserConnect online status +SQUtils.QObject { + id: root + + // SDKs providing the DApps API + required property WalletConnectSDKBase wcSdk + required property WalletConnectSDKBase bcSdk + + // DApps shared store - used for wc peristence and signing requests/transactions + required property DAppsStore store + required property CurrenciesStore currenciesStore + + // Required roles: address + required property var accountsModel + // Required roles: chainId, layer, isOnline + required property var networksModel + // Required roles: tokenKey, balances + required property var groupedAccountAssetsModel + + readonly property alias requestsModel: requests + readonly property alias dappsModel: dappConnections.dappsModel + readonly property bool enabled: wcSdk.enabled || bcSdk.enabled + readonly property bool isServiceOnline: chainsSupervisorPlugin.anyChainAvailable && (wcSdk.sdkReady || bcSdk.enabled) + + // Connection signals + /// Emitted when a new DApp requests a connection + signal connectDApp(var chains, string dAppUrl, string dAppName, string dAppIcon, string key) + /// Emitted when a new DApp is connected + signal dappConnected(string proposalId, string newTopic, string url, int connectorId) + /// Emitted when a DApp is disconnected + signal dappDisconnected(string topic, string url) + /// Emitted when a new DApp fails to connect + signal newConnectionFailed(string key, string dappUrl, var error) + + // Pairing signals + signal pairingValidated(int validationState) + signal pairingResponse(int state) // Maps to Pairing.errors + + // Sign request signals + signal signCompleted(string topic, string id, bool userAccepted, string error) + signal siweCompleted(string topic, string id, string error) + + /// WalletConnect pairing + /// @param uri - the pairing URI to pair + /// Result is emitted via the pairingResponse signal + /// A new session proposal is expected to be emitted if the pairing is successful + function pair(uri) { + return wcSdk.pair(uri) + } + + /// Approves or rejects the session proposal. App response to `connectDApp` + /// @param key - the key of the session proposal + /// @param approvedChainIds - array containing the chainIds that the user approved + /// @param accountAddress - the address of the account that approved the session + function approvePairSession(key, approvedChainIds, accountAddress) { + if (siwePlugin.connectionRequests.has(key.toString())) { + siwePlugin.accept(key, approvedChainIds, accountAddress) + siwePlugin.connectionRequests.delete(key.toString()) + return + } + dappConnections.connect(key, approvedChainIds, accountAddress) + } + + /// Rejects the session proposal. App response to `connectDApp` + /// @param id - the id of the session proposal + function rejectPairSession(id) { + if (siwePlugin.connectionRequests.has(id.toString())) { + siwePlugin.reject(id) + siwePlugin.connectionRequests.delete(id.toString()) + return + } + dappConnections.reject(id) + } + + /// Disconnects the WC session with the given topic. Expected `dappDisconnected` signal + /// @param sessionTopic - the topic of the session to disconnect + function disconnectSession(sessionTopic) { + dappConnections.disconnect(sessionTopic) + } + + /// Validates the pairing URI and emits the pairingValidated signal. Expected `pairingValidated` signal + /// Async function + /// @param uri - the pairing URI to validate + function validatePairingUri(uri){ + const info = DAppsHelpers.extractInfoFromPairUri(uri) + wcSdk.getActiveSessions((sessions) => { + // Check if the URI is already paired + let validationState = Pairing.errors.uriOk + for (const key in sessions) { + if (sessions[key].pairingTopic === info.topic) { + validationState = Pairing.errors.alreadyUsed + break + } + } + + // Check if expired + if (validationState === Pairing.errors.uriOk) { + const now = (new Date().getTime())/1000 + if (info.expiry < now) { + validationState = Pairing.errors.expired + } + } + + root.pairingValidated(validationState) + }); + } + + /// Returns the DApp with the given topic + /// @param topic - the topic of the DApp to return + /// @return the DApp with the given topic + /// DApp { + /// name: string + /// url: string + /// iconUrl: string + /// topic: string + /// connectorId: int + /// accountAddressses: [{address: string}] + /// chains: string + /// rawSessions: [{session: object}] + /// } + function getDApp(topic) { + return SQUtils.ModelUtils.getFirstModelEntryIf(dappsModel, (dapp) => { + return dapp.topic === topic + SQUtils.ModelUtils.getFirstModelEntryIf(dapp.rawSessions, (session) => { + return session.topic === topic + }) + }) + } + + DAppConnectionsPlugin { + id: dappConnections + wcSDK: root.wcSdk + bcSDK: root.bcSdk + dappsStore: root.store + accountsModel: root.accountsModel + networksModel: root.networksModel + + onConnected: (proposalId, topic, url, connectorId) => { + root.dappConnected(proposalId, topic, url, connectorId) + } + onDisconnected: (topic, url) => { + root.dappDisconnected(topic, url) + } + onNewConnectionProposed: (key, chains, dAppUrl, dAppName, dAppIcon) => { + root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key) + } + onNewConnectionFailed: (key, dappUrl, error) => { + root.newConnectionFailed(key, dappUrl, error) + } + } + + SessionRequestsModel { + id: requests + } + + ChainsSupervisorPlugin { + id: chainsSupervisorPlugin + + sdk: root.wcSdk + networksModel: root.networksModel + } + + Connections { + target: root.wcSdk + enabled: root.wcSdk.enabled + + function onPairResponse(ok) { + root.pairingResponse(ok) + } + } + + SiweRequestPlugin { + id: siwePlugin + + readonly property var connectionRequests: new Map() + sdk: root.wcSdk + store: root.store + accountsModel: root.accountsModel + networksModel: root.networksModel + + onRegisterSignRequest: (request) => { + requests.enqueue(request) + } + + onUnregisterSignRequest: (requestId) => { + const request = requests.findById(requestId) + if (request === null) { + console.error("SiweRequestPlugin::onUnregisterSignRequest: Error finding event for requestId", requestId) + return + } + requests.removeRequest(request.topic, requestId) + } + + onConnectDApp: (chains, dAppUrl, dAppName, dAppIcon, key) => { + siwePlugin.connectionRequests.set(key.toString(), {chains, dAppUrl, dAppName, dAppIcon}) + root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key) + } + + onSiweFailed: (id, error, topic) => { + root.siweCompleted(topic, id, error) + } + + onSiweSuccessful: (id, topic) => { + d.lookupSession(topic, function(session) { + // Persist session + if(!root.store.addWalletConnectSession(JSON.stringify(session))) { + console.error("Failed to persist session") + } + + root.siweCompleted(topic, id, "") + }) + } + + function accept(key, approvedChainIds, accountAddress) { + const approvedNamespaces = JSON.parse( + DAppsHelpers.buildSupportedNamespaces(approvedChainIds, + [accountAddress], + SessionRequest.getSupportedMethods())) + siwePlugin.connectionApproved(key, approvedNamespaces) + } + function reject(key) { + siwePlugin.connectionRejected(key) + } + } + + SQUtils.QObject { + id: d + + function lookupSession(topicToLookup, callback) { + wcSdk.getActiveSessions((res) => { + Object.keys(res).forEach((topic) => { + if (topic === topicToLookup) { + let session = res[topic] + callback(session) + } + }) + }) + } + } + + // bcSignRequestPlugin and wcSignRequestPlugin are used to handle sign requests + // Almost identical, and it's worth extracting in an inline component, but Qt5.15.2 doesn't support it + SignRequestPlugin { + id: bcSignRequestPlugin + + sdk: root.bcSdk + groupedAccountAssetsModel: root.groupedAccountAssetsModel + networksModel: root.networksModel + accountsModel: root.accountsModel + currentCurrency: root.currenciesStore.currentCurrency + store: root.store + requests: root.requestsModel + dappsModel: root.dappsModel + + getFiatValue: (value, currency) => { + return root.currenciesStore.getFiatValue(value, currency) + } + + onSignCompleted: (topic, id, userAccepted, error) => { + root.signCompleted(topic, id, userAccepted, error) + } + } + + SignRequestPlugin { + id: wcSignRequestPlugin + + sdk: root.wcSdk + groupedAccountAssetsModel: root.groupedAccountAssetsModel + networksModel: root.networksModel + accountsModel: root.accountsModel + currentCurrency: root.currenciesStore.currentCurrency + store: root.store + requests: root.requestsModel + dappsModel: root.dappsModel + + getFiatValue: (value, currency) => { + return root.currenciesStore.getFiatValue(value, currency) + } + + onSignCompleted: (topic, id, userAccepted, error) => { + root.signCompleted(topic, id, userAccepted, error) + } + } +} diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml b/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml index 543ef0deca..0f1931ef86 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml @@ -1127,6 +1127,8 @@ SQUtils.QObject { } onAuthFailed: () => { + d.unsubscribeForFeeUpdates(request.topic, request.requestId) + sdk.rejectSessionRequest(request.topic, request.requestId, true) const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl) const methodStr = SessionRequest.methodToUserString(request.method) if (!methodStr) { diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsService.qml b/ui/app/AppLayouts/Wallet/services/dapps/DAppsService.qml new file mode 100644 index 0000000000..8f7cd1c197 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/services/dapps/DAppsService.qml @@ -0,0 +1,271 @@ +import QtQuick 2.15 + +import StatusQ 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils + +import AppLayouts.Wallet 1.0 +import AppLayouts.Wallet.services.dapps 1.0 +import AppLayouts.Wallet.services.dapps.types 1.0 +import shared.popups.walletconnect 1.0 + +import SortFilterProxyModel 0.2 +import utils 1.0 + +import "types" + +SQUtils.QObject { + id: root + + required property DAppsModule dappsModule + required property string selectedAddress + required property var accountsModel + + property bool walletConnectFeatureEnabled: true + property bool connectorFeatureEnabled: true + + // Output properties + /// Model contaning all dApps available for the currently selected account + readonly property var dappsModel: d.filteredDappsModel + /// Model containig the dApps session requests to be resolved by the user + readonly property SessionRequestsModel sessionRequestsModel: dappsModule.requestsModel + /// Service can interact with the current address selection + /// Default value: true + readonly property bool serviceAvailableToCurrentAddress: !root.selectedAddress || + SQUtils.ModelUtils.contains(root.accountsModel, "address", root.selectedAddress, Qt.CaseInsensitive) + + readonly property bool isServiceOnline: dappsModule.isServiceOnline + + // signals + signal connectDApp(var dappChains, url dappUrl, string dappName, url dappIcon, string key) + // Emitted as a response to DAppsService.approveSession + // @param key The key of the session proposal + // @param error The error message + // @param topic The new topic of the session + signal approveSessionResult(string key, var error, string topic) + // Emitted when a new session is requested by a dApp + signal sessionRequest(string id) + // Emitted when the services requests to display a toast message + // @param message The message to display + // @param type The type of the message. Maps to Constants.ephemeralNotificationType + signal displayToastMessage(string message, int type) + // Emitted as a response to DAppsService.validatePairingUri or other DAppsService.pair + // and DAppsService.approvePair errors + signal pairingValidated(int validationState) + + // methods + /// Triggers the signing process for the given session request + /// @param topic The topic of the session + /// @param id The id of the session request + function sign(topic, id) { + // The authentication triggers the signing process + // authenticate -> sign -> inform the dApp + d.sign(topic, id) + } + + function rejectSign(topic, id, hasError) { + d.rejectSign(topic, id, hasError) + } + + function signRequestIsLive(topic, id) { + d.signRequestIsLive(topic, id) + } + + /// Validates the pairing URI + function validatePairingUri(uri) { + d.validatePairingUri(uri) + } + + /// Initiates the pairing process with the given URI + function pair(uri) { + timeoutTimer.start() + dappsModule.pair(uri) + } + + /// Approves or rejects the session proposal + function approvePairSession(key, approvedChainIds, accountAddress) { + dappsModule.approvePairSession(key, approvedChainIds, accountAddress) + } + + + /// Rejects the session proposal + function rejectPairSession(id) { + dappsModule.rejectPairSession(id) + } + + /// Disconnects the dApp with the given topic + /// @param topic The topic of the dApp + /// @param source The source of the dApp; either "walletConnect" or "connector" + function disconnectDapp(topic) { + d.disconnectDapp(topic) + } + + SQUtils.QObject { + id: d + + readonly property var filteredDappsModel: SortFilterProxyModel { + id: dappsFilteredModel + objectName: "DAppsModelFiltered" + sourceModel: root.dappsModule.dappsModel + readonly property string selectedAddress: root.selectedAddress + + filters: FastExpressionFilter { + enabled: !!dappsFilteredModel.selectedAddress + + function isAddressIncluded(accountAddressesSubModel, selectedAddress) { + if (!accountAddressesSubModel) { + return false + } + const addresses = SQUtils.ModelUtils.modelToFlatArray(accountAddressesSubModel, "address") + return addresses.includes(selectedAddress) + } + expression: isAddressIncluded(model.accountAddresses, dappsFilteredModel.selectedAddress) + + expectedRoles: "accountAddresses" + } + } + + function disconnectDapp(connectionId) { + dappsModule.disconnectSession(connectionId) + } + + function validatePairingUri(uri) { + // Check if emoji inside the URI + if(Constants.regularExpressions.emoji.test(uri)) { + root.pairingValidated(Pairing.errors.tooCool) + return + } else if(!DAppsHelpers.validURI(uri)) { + root.pairingValidated(Pairing.errors.invalidUri) + return + } + + dappsModule.validatePairingUri(uri) + } + + function signRequestIsLive(topic, id) { + const request = root.sessionRequestsModel.findRequest(topic, id) + if (!request) { + console.error("Session request not found") + return + } + request.setActive() + } + + function sign(topic, id) { + const request = root.sessionRequestsModel.findRequest(topic, id) + if (!request) { + console.error("Session request not found") + return + } + request.accept() + } + + function rejectSign(topic, id, hasError) { + const request = root.sessionRequestsModel.findRequest(topic, id) + if (!request) { + console.error("Session request not found") + return + } + request.reject(hasError) + } + + function reportPairErrorState(state) { + timeoutTimer.stop() + root.pairingValidated(state) + } + } + + Connections { + target: root.dappsModule + enabled: root.walletConnectFeatureEnabled || root.connectorFeatureEnabled + function onPairingValidated(state) { + timeoutTimer.stop() + root.pairingValidated(state) + } + + function onPairingResponse(key, state) { + timeoutTimer.stop() + if (state != Pairing.errors.uriOk) { + d.reportPairErrorState(state) + } + } + + function onConnectDApp(dappChains, dappUrl, dappName, dappIcon, key) { + root.connectDApp(dappChains, dappUrl, dappName, dappIcon, key) + } + + function onDappConnected(proposal, topic, url, connectorId) { + const dappDomain = SQUtils.StringUtils.extractDomainFromLink(url) + const connectorName = connectorId === Constants.WalletConnect ? "WalletConnect" : "Status Connector" + root.displayToastMessage(qsTr("Connected to %1 via %2").arg(dappDomain).arg(connectorName), Constants.ephemeralNotificationType.success) + root.approveSessionResult(proposal, null, topic) + } + + function onDappDisconnected(topic, url) { + const appDomain = SQUtils.StringUtils.extractDomainFromLink(url) + root.displayToastMessage(qsTr("Disconnected from %1").arg(appDomain), Constants.ephemeralNotificationType.success) + } + + function onNewConnectionFailed(key, url, error) { + timeoutTimer.stop() + const dappDomain = SQUtils.StringUtils.extractDomainFromLink(url) + if (error === Pairing.errors.userRejected) { + root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(dappDomain), Constants.ephemeralNotificationType.success) + } + if (error === Pairing.errors.rejectFailed) { + root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(dappDomain), Constants.ephemeralNotificationType.danger) + } + d.reportPairErrorState(error) + root.approveSessionResult(key, error, "") + } + + function onSignCompleted(topic, id, userAccepted, error) { + const request = root.sessionRequestsModel.findRequest(topic, id) + if (!request) { + console.error("Session request not found") + return + } + + const methodStr = SessionRequest.methodToUserString(request.method) + const appUrl = request.dappUrl + const appDomain = SQUtils.StringUtils.extractDomainFromLink(appUrl) + if (!methodStr) { + console.error("Error finding user string for method", request.method) + return + } + + if (error) { + root.displayToastMessage(qsTr("Fail to %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger) + return + } + + const requestExpired = request.isExpired() + if (requestExpired) { + root.displayToastMessage("%1 sign request timed out".arg(appDomain), Constants.ephemeralNotificationType.normal) + return + } + + const actionStr = userAccepted ? qsTr("accepted") : qsTr("rejected") + root.displayToastMessage("%1 %2 %3".arg(appDomain).arg(methodStr).arg(actionStr), Constants.ephemeralNotificationType.success) + } + + function onSiweCompleted(topic, id, userAccepted, error) { + if (error) { + root.displayToastMessage(error, Constants.ephemeralNotificationType.danger) + } + root.approveSessionResult(id, error, topic) + } + } + + // Timeout for the corner case where the URL was already dismissed and the SDK doesn't respond with an error nor advances with the proposal + Timer { + id: timeoutTimer + + interval: 10000 // (10 seconds) + running: false + repeat: false + + onTriggered: { + d.reportPairErrorState(Pairing.errors.unknownError) + } + } +} diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DappsConnectorSDK.qml b/ui/app/AppLayouts/Wallet/services/dapps/DappsConnectorSDK.qml index f9c7d76436..f4e5290bdd 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/DappsConnectorSDK.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/DappsConnectorSDK.qml @@ -21,773 +21,126 @@ import utils 1.0 import "types" -// Act as another layer of abstraction to the WalletConnectSDKBase -// Quick hack until the WalletConnectSDKBase could be refactored to a more generic DappProviderBase with API to match -// the UX requirements +/// Act as another layer of abstraction to the WalletConnectSDKBase +/// Converts the store requests into WalletConnect standard requests WalletConnectSDKBase { id: root - - required property WalletConnectService wcService - required property var walletStore - required property DAppsStore store - required property int loginType - - property var controller - property var dappInfo: null - property var txArgs: null - property bool sdkReady: true - property bool active: true - property string requestId: "" - property alias requestsModel: requests - - readonly property string invalidDAppUrlError: "Invalid dappInfo: URL is missing" - readonly property string invalidDAppTopicError: "Invalid dappInfo: failed to parse topic" + + required property BrowserConnectStore store + /// Required roles: chainId + required property var networksModel + /// Required roles: address + required property var accountsModel projectId: "" - implicitWidth: 1 - implicitHeight: 1 - - // TODO Refactor this code to avoid code duplication from Wallet Connect DAppsRequestHandler - // https://github.com/status-im/status-desktop/issues/15711 - QtObject { - id: d - - function sessionRequestEvent(event) { - let obj = d.resolveAsync(event) - if (obj === null) { - let error = true - controller.rejectTransactionSigning(root.requestId) - return - } - sessionRequestLoader.request = obj - requests.enqueue(obj) - } - - function resolveAsync(event) { - let method = event.params.request.method - let accountAddress = lookupAccountFromEvent(event, method) - if(!accountAddress) { - console.error("Error finding accountAddress for event", JSON.stringify(event)) - return null - } - let chainId = lookupNetworkFromEvent(event, method) - if(!chainId) { - console.error("Error finding network for event", JSON.stringify(event)) - return null - } - let data = extractMethodData(event, method) - if(!data) { - console.error("Error in event data lookup", JSON.stringify(event)) - return null - } - const interpreted = d.prepareData(method, data) - let enoughFunds = !isTransactionMethod(method) - let obj = sessionRequestComponent.createObject(null, { - event, - topic: event.topic, - requestId: event.id, - method, - accountAddress, - chainId, - data, - preparedData: interpreted.preparedData, - maxFeesText: "?", - maxFeesEthText: "?", - haveEnoughFunds: enoughFunds - }) - - if (obj === null) { - console.error("Error creating SessionRequestResolved for event") - return null - } - - // Check later to have a valid request object - if (!SessionRequest.getSupportedMethods().includes(method)) { - console.error("Unsupported method", method) - return null - } - - let session = getActiveSession(root.dappInfo) - - if (session === null) { - console.error("Connector.lookupSession: error finding session for requestId ", obj.requestId) - return - } - obj.resolveDappInfoFromSession(session) - - if (d.isTransactionMethod(method)) { - let tx = obj.data.tx - if (tx === null) { - console.error("Error cannot resolve tx object") - return null - } - let BigOps = SQUtils.AmountsArithmetic - let gasLimit = hexToGwei(tx.gasLimit) - - if (tx.gasPrice === null || tx.gasPrice === undefined) { - let maxFeePerGas = hexToGwei(tx.maxFeePerGas) - let maxPriorityFeePerGas = hexToGwei(tx.maxPriorityFeePerGas) - let totalMaxFees = BigOps.sum(maxFeePerGas, maxPriorityFeePerGas) - let maxFees = BigOps.times(gasLimit, totalMaxFees) - let maxFeesString = maxFees.toString() - obj.maxFeesText = maxFeesString - obj.maxFeesEthText = maxFeesString - obj.haveEnoughFunds = true - } else { - let gasPrice = hexToGwei(tx.gasPrice) - let maxFees = BigOps.times(gasLimit, gasPrice) - let maxFeesString = maxFees.toString() - obj.maxFeesText = maxFeesString - obj.maxFeesEthText = maxFeesString - obj.haveEnoughFunds = true - } - } - - return obj - } - - function getTxObject(method, data) { - let tx - if (method === SessionRequest.methods.signTransaction.name) { - tx = SessionRequest.methods.signTransaction.getTxObjFromData(data) - } else if (method === SessionRequest.methods.sendTransaction.name) { - tx = SessionRequest.methods.sendTransaction.getTxObjFromData(data) - } else { - console.error("Not a transaction method") - } - return tx - } - - // returns { - // preparedData, - // value // null or ETH Big number - // } - function prepareData(method, data) { - let payload = null - switch(method) { - case SessionRequest.methods.personalSign.name: { - payload = SessionRequest.methods.personalSign.getMessageFromData(data) - break - } - case SessionRequest.methods.sign.name: { - payload = SessionRequest.methods.sign.getMessageFromData(data) - break - } - case SessionRequest.methods.signTypedData_v4.name: { - const stringPayload = SessionRequest.methods.signTypedData_v4.getMessageFromData(data) - payload = JSON.stringify(JSON.parse(stringPayload), null, 2) - break - } - case SessionRequest.methods.signTypedData.name: { - const stringPayload = SessionRequest.methods.signTypedData.getMessageFromData(data) - payload = JSON.stringify(JSON.parse(stringPayload), null, 2) - break - } - case SessionRequest.methods.signTransaction.name: - case SessionRequest.methods.sendTransaction.name: - // For transactions we process the data in a different way as follows - break - default: - console.error("Unhandled method", method) - break; - } - - let value = SQUtils.AmountsArithmetic.fromNumber(0) - if (d.isTransactionMethod(method)) { - let txObj = d.getTxObject(method, data) - let tx = Object.assign({}, txObj) - if (tx.value) { - value = hexToEth(tx.value) - tx.value = value.toString() - } - if (tx.maxFeePerGas) { - tx.maxFeePerGas = hexToGwei(tx.maxFeePerGas).toString() - } - if (tx.maxPriorityFeePerGas) { - tx.maxPriorityFeePerGas = hexToGwei(tx.maxPriorityFeePerGas).toString() - } - if (tx.gasPrice) { - tx.gasPrice = hexToGwei(tx.gasPrice) - } - if (tx.gasLimit) { - tx.gasLimit = parseInt(root.store.hexToDec(tx.gasLimit)) - } - if (tx.nonce) { - tx.nonce = parseInt(root.store.hexToDec(tx.nonce)) - } - - payload = JSON.stringify(tx, null, 2) - } - return { - preparedData: payload, - value: value - } - } - - function hexToEth(value) { - return hexToEthDenomination(value, "eth") - } - function hexToGwei(value) { - return hexToEthDenomination(value, "gwei") - } - function hexToEthDenomination(value, ethUnit) { - let unitMapping = { - "gwei": 9, - "eth": 18 - } - let BigOps = SQUtils.AmountsArithmetic - let decValue = root.store.hexToDec(value) - if (!!decValue) { - return BigOps.div(BigOps.fromNumber(decValue), BigOps.fromNumber(1, unitMapping[ethUnit])) - } - return BigOps.fromNumber(0) - } - - function isTransactionMethod(method) { - return method === SessionRequest.methods.signTransaction.name - || method === SessionRequest.methods.sendTransaction.name - } - - /// Returns null if the account is not found - function lookupAccountFromEvent(event, method) { - var address = "" - if (method === SessionRequest.methods.personalSign.name) { - if (event.params.request.params.length < 2) { - return address - } - address = event.params.request.params[1] - } else if (method === SessionRequest.methods.sign.name) { - if (event.params.request.params.length === 1) { - return address - } - address = event.params.request.params[0] - } else if(method === SessionRequest.methods.signTypedData_v4.name || - method === SessionRequest.methods.signTypedData.name) - { - if (event.params.request.params.length < 2) { - return address - } - address = event.params.request.params[0] - } else if (method === SessionRequest.methods.signTransaction.name - || method === SessionRequest.methods.sendTransaction.name) { - if (event.params.request.params.length == 0) { - return address - } - address = event.params.request.params[0].from - } else { - console.error("Unsupported method to lookup account: ", method) - return null - } - const account = SQUtils.ModelUtils.getFirstModelEntryIf(root.wcService.validAccounts, (account) => { - return account.address.toLowerCase() === address.toLowerCase(); - }) - - if (!account) { - return address - } - - return account.address - } - - /// Returns null if the network is not found - function lookupNetworkFromEvent(event, method) { - if (SessionRequest.getSupportedMethods().includes(method) === false) { - return null - } - const chainId = DAppsHelpers.chainIdFromEip155(event.params.chainId) - const network = SQUtils.ModelUtils.getByKey(root.walletStore.filteredFlatModel, "chainId", chainId) - - if (!network) { - return null - } - - return network.chainId - } - - function extractMethodData(event, method) { - if (method === SessionRequest.methods.personalSign.name || - method === SessionRequest.methods.sign.name) - { - if (event.params.request.params.length < 1) { - return null - } - let message = "" - const messageIndex = (method === SessionRequest.methods.personalSign.name ? 0 : 1) - const messageParam = event.params.request.tx.data - - // There is no standard on how data is encoded. Therefore we support hex or utf8 - if (DAppsHelpers.isHex(messageParam)) { - message = DAppsHelpers.hexToString(messageParam) - } else { - message = messageParam - } - return SessionRequest.methods.personalSign.buildDataObject(message) - } else if (method === SessionRequest.methods.signTypedData_v4.name || - method === SessionRequest.methods.signTypedData.name) - { - if (event.params.request.params.length < 2) { - return null - } - let jsonMessage = event.params.request.params[1] - let methodObj = method === SessionRequest.methods.signTypedData_v4.name - ? SessionRequest.methods.signTypedData_v4 - : SessionRequest.methods.signTypedData - return methodObj.buildDataObject(jsonMessage) - } else if (method === SessionRequest.methods.signTransaction.name) { - if (event.params.request.params.length == 0) { - return null - } - let tx = event.params.request.params[0] - return SessionRequest.methods.signTransaction.buildDataObject(tx) - } else if (method === SessionRequest.methods.sendTransaction.name) { - if (event.params.request.params.length == 0) { - return null - } - const tx = event.params.request.params[0] - return SessionRequest.methods.sendTransaction.buildDataObject(tx) - } else { - return null - } - } - - function executeSessionRequest(request, password, pin) { - if (!SessionRequest.getSupportedMethods().includes(request.method)) { - console.error("Unsupported method to execute: ", request.method) - return - } - - if (password === "") { - console.error("No password provided to sign message") - return - } - - if (request.method === SessionRequest.methods.sign.name) { - store.signMessageUnsafe(request.topic, - request.requestId, - request.accountAddress, - SessionRequest.methods.personalSign.getMessageFromData(request.data), - password, - pin) - } else if (request.method === SessionRequest.methods.personalSign.name) { - store.signMessage(request.topic, - request.requestId, - request.accountAddress, - SessionRequest.methods.personalSign.getMessageFromData(request.data), - password, - pin) - } else if (request.method === SessionRequest.methods.signTypedData_v4.name || - request.method === SessionRequest.methods.signTypedData.name) - { - let legacy = request.method === SessionRequest.methods.signTypedData.name - store.safeSignTypedData(request.topic, - request.requestId, - request.accountAddress, - SessionRequest.methods.signTypedData.getMessageFromData(request.data), - request.chainId, - legacy, - password, - pin) - } else if (request.method === SessionRequest.methods.signTransaction.name) { - let txObj = SessionRequest.methods.signTransaction.getTxObjFromData(request.data) - store.signTransaction(request.topic, - request.requestId, - request.accountAddress, - request.chainId, - txObj, - password, - pin) - } else if (request.method === SessionRequest.methods.sendTransaction.name) { - store.sendTransaction(request.topic, - request.requestId, - request.accountAddress, - request.chainId, - request.data.tx, - password, - pin) - } - } - - function acceptSessionRequest(topic, id, signature) { - console.debug(`Connector DappsConnectorSDK.acceptSessionRequest; requestId: ${root.requestId}, signature: "${signature}"`) - - sessionRequestLoader.active = false - controller.approveTransactionRequest(requestId, signature) - - root.wcService.displayToastMessage(qsTr("Successfully signed transaction from %1").arg(root.dappInfo.url), false) - } - - function getActiveSession(dappInfos) { - let sessionTemplate = (dappUrl, dappName, dappIcon) => { - return { - "peer": { - "metadata": { - "description": "-", - "icons": [ - dappIcon - ], - "name": dappName, - "url": dappUrl - } - }, - "topic": dappUrl - }; - } - - let session = root.wcService.connectorDAppsProvider.getActiveSession(dappInfos.url) - if (!session) { - console.error("Connector.lookupSession: error finding session for requestId ", root.requestId) - - return - } - - return sessionTemplate(session.url, session.name, session.icon) - } - - function authenticate(request) { - return store.authenticateUser(request.topic, request.requestId, request.accountAddress) - } - } - Connections { target: root.store + enabled: root.enabled - function onUserAuthenticated(topic, id, password, pin) { - var request = requests.findRequest(topic, id) - if (request === null) { - console.error(">Error finding event for topic", topic, "id", id) - return - } - d.executeSessionRequest(request, password, pin) - } + function onSignRequested(requestId, dappInfoString) { + try { + var dappInfo = JSON.parse(dappInfoString) + var txArgsParams = JSON.parse(dappInfo.txArgs) + let event = d.buildSessionRequest(requestId, dappInfo.url, dappInfo.chainId, SessionRequest.methods.sendTransaction.name, txArgsParams) - function onUserAuthenticationFailed(topic, id) { - var request = requests.findRequest(topic, id) - let methodStr = SessionRequest.methodToUserString(request.method) - if (request === null || !methodStr) { - return - } - d.lookupSession(topic, function(session) { - if (session === null) - return - root.displayToastMessage(qsTr("Failed to authenticate %1").arg(session.peer.metadata.url), true) - }) - } - - function onSigningResult(topic, id, data) { - let isSuccessful = (data != "") - if (isSuccessful) { - // acceptSessionRequest will trigger an sdk.sessionRequestUserAnswerResult signal - d.acceptSessionRequest(topic, id, data) - } else { - console.error("signing error") + root.sessionRequestEvent(event) + } catch (e) { + console.error("Failed to parse dappInfo for session request", e) } } - } - Loader { - id: connectDappLoader - - active: false - - property var dappChains: [] - property var sessionProposal: null - property var availableNamespaces: null - property var sessionTopic: null - readonly property var proposalMedatada: !!sessionProposal - ? sessionProposal.params.proposer.metadata - : { name: "", url: "", icons: [] } - - sourceComponent: ConnectDAppModal { - visible: true - - onClosed: { - rejectSession(root.requestId) - connectDappLoader.active = false - } - flatNetworks: root.walletStore.filteredFlatModel - accounts: root.wcService.validAccounts - - dAppUrl: proposalMedatada.url - dAppName: proposalMedatada.name - dAppIconUrl: !!proposalMedatada.icons && proposalMedatada.icons.length > 0 ? proposalMedatada.icons[0] : "" - multipleChainSelection: false - - onConnect: { - connectDappLoader.active = false - approveSession(root.requestId, selectedAccount.address, selectedChains) - } - - onDecline: { - connectDappLoader.active = false - rejectSession(root.requestId) - } - - onDisconnect: { - connectDappLoader.active = false; - controller.recallDAppPermission(root.dappInfo.url) + function onConnectRequested(requestId, dappInfoString) { + try { + var dappInfo = JSON.parse(dappInfoString) + dappInfo.proposal = d.buildSessionProposal(requestId, dappInfo.url, dappInfo.name, dappInfo.icon, SessionRequest.getSupportedMethods()) + d.sessionRequests.set(requestId, dappInfo) + root.sessionProposal(dappInfo.proposal) + } catch (e) { + console.error("Failed to parse dappInfo for connection request", e) } } - } - Loader { - id: sessionRequestLoader - - active: false - - onLoaded: item.open() - - property SessionRequestResolved request: null - - property var dappInfo: null - - sourceComponent: DAppSignRequestModal { - id: dappRequestModal - objectName: "connectorDappsRequestModal" - - readonly property var account: accountEntry.available ? accountEntry.item : { - "address": "", - "name": "", - "emoji": "", - "colorId": 0 - } - - readonly property var network: networkEntry.available ? networkEntry.item : { - "chainId": 0, - "chainName": "", - "iconUrl": "" - } - - loginType: account.migragedToKeycard ? Constants.LoginType.Keycard : root.loginType - formatBigNumber: (number, symbol, noSymbolOption) => root.wcService.walletRootStore.currencyStore.formatBigNumber(number, symbol, noSymbolOption) - - visible: true - - dappName: request.dappName - dappUrl: request.dappUrl - dappIcon: request.dappIcon - - accountColor: Utils.getColorForId(account.colorId) - accountName: account.name - accountAddress: account.address - accountEmoji: account.emoji - - networkName: network.chainName - networkIconPath: Theme.svg(network.iconUrl) - - fiatFees: request.maxFeesText - cryptoFees: request.maxFeesEthText - estimatedTime: "" - feesLoading: !request.maxFeesText || !request.maxFeesEthText - hasFees: signingTransaction - enoughFundsForTransaction: request.haveEnoughFunds - enoughFundsForFees: request.haveEnoughFunds - - signingTransaction: request.method === SessionRequest.methods.signTransaction.name || request.method === SessionRequest.methods.sendTransaction.name - - requestPayload: { - switch(request.method) { - case SessionRequest.methods.personalSign.name: - return SessionRequest.methods.personalSign.getMessageFromData(request.data) - case SessionRequest.methods.sign.name: { - return SessionRequest.methods.sign.getMessageFromData(request.data) - } - case SessionRequest.methods.signTypedData_v4.name: { - const stringPayload = SessionRequest.methods.signTypedData_v4.getMessageFromData(request.data) - return JSON.stringify(JSON.parse(stringPayload), null, 2) - } - case SessionRequest.methods.signTypedData.name: { - const stringPayload = SessionRequest.methods.signTypedData.getMessageFromData(root.payloadData) - return JSON.stringify(JSON.parse(stringPayload), null, 2) - } - case SessionRequest.methods.signTransaction.name: { - const jsonPayload = SessionRequest.methods.signTransaction.getTxObjFromData(request.data) - return JSON.stringify(jsonPayload, null, 2) - } - case SessionRequest.methods.sendTransaction.name: { - const jsonPayload = SessionRequest.methods.sendTransaction.getTxObjFromData(request.data) - return JSON.stringify(request.data, null, 2) - } - } - } - - onClosed: { - Qt.callLater( () => { - sessionRequestLoader.active = false - }) - } - - onAccepted: { - if (!request) { - console.error("Error signing: request is null") - return - } - - d.authenticate(request) - } - - onRejected: { - sessionRequestLoader.active = false - controller.rejectTransactionSigning(root.requestId) - root.wcService.displayToastMessage(qsTr("Failed to sign transaction from %1").arg(request.dappUrl), true) - } - - ModelEntry { - id: networkEntry - sourceModel: root.wcService.flatNetworks - key: "chainId" - value: request.chainId - } - - ModelEntry { - id: accountEntry - sourceModel: root.wcService.validAccounts - key: "address" - value: request.accountAddress + function onDisconnected(dappInfoString) { + try { + let dappItem = JSON.parse(dappInfoString) + root.sessionDelete(dappItem.url, false) + } catch (e) { + console.error("Failed to parse dappInfo for disconnection", e) } } - } - Component { - id: sessionRequestComponent - - SessionRequestResolved { - sourceId: Constants.DAppConnectors.StatusConnect - } - } - - SessionRequestsModel { - id: requests - } - - Connections { - target: root.wcService - - function onRevokeSession(topic) { - if (!topic) { - console.warn(invalidDAppTopicError) - return + function onApproveTransactionResponse(topic, requestId, error) { + try { + const errorStr = error ? "Faled to approve trasnsaction" : "" + root.sessionRequestUserAnswerResult(topic, requestId, true, errorStr) + } catch (e) { + console.error("Failed to approve transaction response", e) } - - controller.recallDAppPermission(topic) - root.wcService.connectorDAppsProvider.revokeSession(topic) - } - } - - Connections { - target: controller - - onDappValidatesTransaction: function(requestId, dappInfoString) { - var dappInfo = JSON.parse(dappInfoString) - root.dappInfo = dappInfo - var txArgsParams = JSON.parse(dappInfo.txArgs) - root.txArgs = txArgsParams - let event = { - "id": root.requestId, - "topic": dappInfo.url, - "params": { - "chainId": `eip155:${dappInfo.chainId}`, - "request": { - "method": SessionRequest.methods.sendTransaction.name, - "params": [ - { - "from": txArgsParams.from, - "to": txArgsParams.to, - "value": txArgsParams.value, - "gasLimit": txArgsParams.gas, - "gasPrice": txArgsParams.gasPrice, - "maxFeePerGas": txArgsParams.maxFeePerGas, - "maxPriorityFeePerGas": txArgsParams.maxPriorityFeePerGas, - "nonce": txArgsParams.nonce, - "data": txArgsParams.data - } - ] - } - } - } - - d.sessionRequestEvent(event) - - sessionRequestLoader.active = true - root.requestId = requestId } - onDappRequestsToConnect: function(requestId, dappInfoString) { - var dappInfo = JSON.parse(dappInfoString) - root.dappInfo = dappInfo - let sessionProposal = { - "params": { - "optionalNamespaces": {}, - "proposer": { - "metadata": { - "description": "-", - "icons": [ - dappInfo.icon - ], - "name": dappInfo.name, - "url": dappInfo.url - } - }, - "requiredNamespaces": { - "eip155": { - "chains": [ - `eip155:${dappInfo.chainId}` - ], - "events": [], - "methods": [SessionRequest.methods.personalSign.name] - } - } - } - }; - - connectDappLoader.sessionProposal = sessionProposal - connectDappLoader.active = true - root.requestId = requestId - } - - onDappGrantDAppPermission: function(dappInfoString) { - let dappItem = JSON.parse(dappInfoString) - const { url, name, icon: iconUrl } = dappItem - - if (!url) { - console.warn(invalidDAppUrlError) - return + function onRejectTransactionResponse(topic, requestId, error) { + try { + const errorStr = error ? "Faled to reject trasnsaction" : "" + root.sessionRequestUserAnswerResult(topic, requestId, false, errorStr) + } catch (e) { + console.error("Failed to reject transaction response", e) } - - root.wcService.connectorDAppsProvider.addSession(url, name, iconUrl) - } - - onDappRevokeDAppPermission: function(dappInfoString) { - let dappItem = JSON.parse(dappInfoString) - let session = { - "url": dappItem.url, - "name": dappItem.name, - "iconUrl": dappItem.icon, - "topic": dappItem.url - } - - if (!session.url) { - console.warn(invalidDAppUrlError) - return - } - root.wcService.connectorDAppsProvider.revokeSession(JSON.stringify(session)) - root.wcService.displayToastMessage(qsTr("Disconnected from %1").arg(dappItem.url), false) } } approveSession: function(requestId, account, selectedChains) { - controller.approveDappConnectRequest(requestId, account, JSON.stringify(selectedChains)) - const { url, name, icon: iconUrl } = root.dappInfo; - //TODO: temporary solution until we have a proper way to handle accounts - //The dappProvider should add a new session only when the backend has validated the connection - //Currently the dapp info is limited to the url, name and icon - root.wcService.connectorDAppsProvider.addSession(url, name, iconUrl, account) - root.wcService.displayToastMessage(qsTr("Successfully authenticated %1").arg(url), false); + try { + if (!d.sessionRequests.has(requestId)) { + console.error("Session request not found") + return + } + const dappInfo = d.sessionRequests.get(requestId) + root.store.approveConnection(requestId, account, JSON.stringify(selectedChains)) + const newSession = d.buildSession(dappInfo.url, dappInfo.name, dappInfo.icon, requestId, account, selectedChains) + root.approveSessionResult(requestId, newSession, "") + d.sessionRequests.delete(requestId) + } catch (e) { + console.error("Failed to approve session", e) + } } rejectSession: function(requestId) { - controller.rejectDappConnectRequest(requestId) - root.wcService.displayToastMessage(qsTr("Failed to authenticate %1").arg(root.dappInfo.url), true) + try { + root.store.rejectConnection(requestId) + root.rejectSessionResult(requestId, "") + d.sessionRequests.delete(requestId) + } catch (e) { + console.error("Failed to reject session", e) + } + } + + acceptSessionRequest: function(topic, requestId, signature) { + root.store.approveTransaction(topic, requestId, signature) + } + + rejectSessionRequest: function(topic, requestId, error) { + root.store.rejectTransaction(topic, requestId, error) + } + + disconnectSession: function(topic) { + root.store.disconnect(topic) + } + + getActiveSessions: function(callback) { + try { + const dappsStr = root.store.getDApps() + const dapps = JSON.parse(dappsStr) + let activeSessions = {} + for (let i = 0; i < dapps.length; i++) { + const dapp = dapps[i] + activeSessions[dapp.url] = d.buildSession(dapp.url, dapp.name, dapp.iconUrl, "", dapp.sharedAccount, [dapp.chainId]) + } + callback(activeSessions) + } catch (e) { + console.error("Failed to get active sessions", e) + callback([]) + } } // We don't expect requests for these. They are here only to spot errors @@ -795,4 +148,86 @@ WalletConnectSDKBase { getPairings: function(callback) { console.error("ConnectorSDK.getPairings: not implemented") } disconnectPairing: function(topic) { console.error("ConnectorSDK.disconnectPairing: not implemented") } buildApprovedNamespaces: function(params, supportedNamespaces) { console.error("ConnectorSDK.buildApprovedNamespaces: not implemented") } + + QtObject { + id: d + readonly property var sessionRequests: new Map() + function buildSession(dappUrl, dappName, dappIcon, proposalId, account, chains) { + let sessionTemplate = (dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains) => { + return { + peer: { + metadata: { + description: "-", + icons: [ + dappIcon + ], + name: dappName, + url: dappUrl + } + }, + namespaces: { + eip155: { + accounts: [eipAccount], + chains: eipChains + } + }, + pairingTopic: proposalId, + topic: dappUrl + }; + } + + const eipAccount = account ? `eip155:${account}` : "" + const eipChains = chains ? chains.map((chain) => `eip155:${chain}`) : [] + + return sessionTemplate(dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains) + } + + function buildSessionRequest(requestId, topic, chainId, method, txArgs) { + return { + id: requestId, + topic, + params: { + chainId: `eip155:${chainId}`, + request: { + method: SessionRequest.methods.sendTransaction.name, + params: [ + { + from: txArgs.from, + to: txArgs.to, + value: txArgs.value, + gasLimit: txArgs.gas, + gasPrice: txArgs.gasPrice, + maxFeePerGas: txArgs.maxFeePerGas, + maxPriorityFeePerGas: txArgs.maxPriorityFeePerGas, + nonce: txArgs.nonce, + data: txArgs.data + } + ] + } + } + } + } + + function buildSessionProposal(id, url, name, icon, methods) { + const supportedNamespaces = DAppsHelpers.buildSupportedNamespacesFromModels(root.networksModel, root.accountsModel, methods) + const proposal = { + id, + params: { + optionalNamespaces: {}, + proposer: { + metadata: { + description: "-", + icons: [ + icon + ], + name, + url + } + }, + requiredNamespaces: supportedNamespaces + } + } + return proposal + } + } } diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WCDappsProvider.qml b/ui/app/AppLayouts/Wallet/services/dapps/WCDappsProvider.qml index d7d35b5258..114ca63433 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WCDappsProvider.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WCDappsProvider.qml @@ -1,7 +1,7 @@ import QtQuick 2.15 import StatusQ 0.1 -import StatusQ.Core.Utils 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils import AppLayouts.Wallet.services.dapps 1.0 @@ -19,7 +19,50 @@ DAppsModel { readonly property int connectorId: Constants.WalletConnect - property bool enabled: true + readonly property bool enabled: sdk.enabled + + signal disconnected(string topic, string dAppUrl) + signal connected(string proposalId, string topic, string dAppUrl) + + Connections { + target: root.sdk + enabled: root.enabled + + function onSessionDelete(topic, err) { + const dapp = root.getByTopic(topic) + if (!dapp) { + console.warn("DApp not found for topic - cannot delete session", topic) + return + } + + root.store.deactivateWalletConnectSession(topic) + d.updateDappsModel() + root.disconnected(topic, dapp.url) + } + function onSdkInit(success, result) { + if (!success) { + return + } + d.updateDappsModel() + } + function onApproveSessionResult(proposalId, session, error) { + if (error) { + return + } + + root.store.addWalletConnectSession(JSON.stringify(session)) + d.updateDappsModel() + root.connected(proposalId, session.topic, session.peer.metadata.url) + } + + function onAcceptSessionAuthenticateResult(id, result, error) { + if (error) { + return + } + d.updateDappsModel() + root.connected(id, result.topic, result.session.peer.metadata.url) + } + } Component.onCompleted: { if (!enabled) { @@ -37,34 +80,9 @@ DAppsModel { } } - QObject { + SQUtils.QObject { id: d - property Connections sdkConnections: Connections { - target: root.sdk - enabled: root.enabled - - function onSessionDelete(topic, err) { - d.updateDappsModel() - } - function onSdkInit(success, result) { - if (success) { - d.updateDappsModel() - } - } - function onApproveSessionResult(topic, success, result) { - if (success) { - d.updateDappsModel() - } - } - - function onAcceptSessionAuthenticateResult(id, result, error) { - if (!error) { - d.updateDappsModel() - } - } - } - property var dappsListReceivedFn: null property var getActiveSessionsFn: null function updateDappsModel() @@ -82,7 +100,7 @@ DAppsModel { name: cachedEntry.name, iconUrl: cachedEntry.iconUrl, accountAddresses: [], - topic: "", + topic: cachedEntry.url, connectorId: root.connectorId, rawSessions: [] } @@ -99,12 +117,16 @@ DAppsModel { getActiveSessionsFn = () => { sdk.getActiveSessions((allSessionsAllProfiles) => { + if (!allSessionsAllProfiles) { + console.warn("Failed to get active sessions") + return + } + root.store.dappsListReceived.disconnect(dappsListReceivedFn); const dAppsMap = {} const topics = [] - const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(allSessionsAllProfiles, supportedAccountsModel) - + const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(allSessionsAllProfiles, root.supportedAccountsModel) for (const sessionID in sessions) { const session = sessions[sessionID] const dapp = session.peer.metadata @@ -120,7 +142,7 @@ DAppsModel { // more modern syntax (ES-6) is not available yet const combinedAddresses = new Set(existingDApp.accountAddresses.concat(accounts)); existingDApp.accountAddresses = Array.from(combinedAddresses); - dapp.rawSessions = [...existingDApp.rawSessions, session] + existingDApp.rawSessions = [...existingDApp.rawSessions, session] } else { dapp.accountAddresses = accounts dapp.topic = sessionID diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml index ee46359e56..e2f11811b2 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml @@ -13,10 +13,8 @@ import "types" WalletConnectSDKBase { id: root - readonly property alias sdkReady: d.sdkReady + readonly property bool sdkReady: d.sdkReady - // Enable the WalletConnect SDK - property alias enableSdk: loader.active property alias userUID: loader.profileName readonly property alias url: loader.url @@ -509,7 +507,7 @@ WalletConnectSDKBase { } function onRejectSessionAuthenticateResult(id, result, error) { - //console.debug(`WC WalletConnectSDK.onRejectSessionAuthenticateResult; id: ${id}, result: ${result}, error: ${error}`) + console.debug(`WC WalletConnectSDK.onRejectSessionAuthenticateResult; id: ${id}, result: ${result}, error: ${error}`) root.rejectSessionAuthenticateResult(id, result, error) } } @@ -518,7 +516,7 @@ WalletConnectSDKBase { id: loader anchors.fill: parent - + active: root.enabled url: "qrc:/app/AppLayouts/Wallet/services/dapps/sdk/src/index.html" webChannelObjects: [ statusObject ] @@ -528,5 +526,11 @@ WalletConnectSDKBase { onPageLoadingError: function(error) { console.error("WebEngineLoader.onPageLoadingError: ", error) } + + onActiveChanged: function() { + if (!active) { + d.sdkReady = false + } + } } } diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDKBase.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDKBase.qml index 3866f204b7..518ab91e97 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDKBase.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDKBase.qml @@ -3,24 +3,25 @@ import QtQuick 2.15 /// SDK requires a visible parent to embed WebEngineView Item { required property string projectId + property bool enabled: true signal statusChanged(string message) signal sdkInit(bool success, var result) signal pairResponse(bool success) signal sessionProposal(var sessionProposal) signal sessionProposalExpired() - signal buildApprovedNamespacesResult(var id, var session, string error) - signal approveSessionResult(var proposalId, var approvedNamespaces, string error) - signal rejectSessionResult(var proposalId, string error) - signal sessionRequestExpired(var id) + signal buildApprovedNamespacesResult(string id, var session, string error) + signal approveSessionResult(string proposalId, var approvedNamespaces, string error) + signal rejectSessionResult(string proposalId, string error) + signal sessionRequestExpired(string id) signal sessionRequestEvent(var sessionRequest) signal sessionRequestUserAnswerResult(string topic, string id, bool accept /* not reject */, string error) signal sessionAuthenticateRequest(var sessionData) - signal populateAuthPayloadResult(var id, var authPayload, string error) - signal formatAuthMessageResult(var id, var request, string error) - signal acceptSessionAuthenticateResult(var id, var result, string error) - signal rejectSessionAuthenticateResult(var id, var result, string error) - signal buildAuthObjectResult(var id, var authObject, string error) + signal populateAuthPayloadResult(string id, var authPayload, string error) + signal formatAuthMessageResult(string id, var request, string error) + signal acceptSessionAuthenticateResult(string id, var result, string error) + signal rejectSessionAuthenticateResult(string id, var result, string error) + signal buildAuthObjectResult(string id, var authObject, string error) signal sessionDelete(var topic, string error) diff --git a/ui/app/AppLayouts/Wallet/services/dapps/plugins/DAppConnectionsPlugin.qml b/ui/app/AppLayouts/Wallet/services/dapps/plugins/DAppConnectionsPlugin.qml new file mode 100644 index 0000000000..9da07c5066 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/services/dapps/plugins/DAppConnectionsPlugin.qml @@ -0,0 +1,353 @@ +import QtQuick 2.15 + +import StatusQ 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils + +import AppLayouts.Wallet.services.dapps 1.0 +import AppLayouts.Wallet.services.dapps.types 1.0 + +import shared.stores 1.0 +import utils 1.0 + +// Plugin providing the connection handling for dApps +// Features provided: +// 1. connect +// 2. disconnect +// 3. active connections model +SQUtils.QObject { + id: root + + required property WalletConnectSDKBase wcSDK + required property WalletConnectSDKBase bcSDK + required property DAppsStore dappsStore + // Required roles: address + required property var accountsModel + // Required roles: chainId + required property var networksModel + + // Output model with the following roles: + // - name: string (optional) + // - url: string (required) + // - iconUrl: string (optional) + // - topic: string (required) + // - connectorId: int (required) + // - accountAddressses: [{address: string}] (required) + // - chains: string (optional) + // - rawSessions: [{session: object}] (optional) + readonly property ConcatModel dappsModel: dappsModel + + // Output signal when a dApp is disconnected + signal disconnected(string topic, string dAppUrl) + // Output signal when a new connection is proposed + signal connected(string proposalId, string topic, string dAppUrl, int connectorId) + // Output signal when a new connection is proposed by the SDK + signal newConnectionProposed(string key, var chains, string dAppUrl, string dAppName, string dAppIcon) + // Output signal when a new connection is failed + signal newConnectionFailed(string key, string dappUrl, int errorCode) + + // Request to disconnect a dApp identified by the topic + function disconnect(topic) { + d.disconnect(topic) + } + + // Request to connect a dApp identified by the proposal key + // chains: array of chainIds + // accountAddress: string + function connect(key, chains, accoutAddress) { + d.connect(key, chains, accoutAddress) + } + + // Request to reject a dApp connection request identified by the proposal key + function reject(key) { + d.reject(key) + } + + WCDappsProvider { + id: dappsProvider + sdk: root.wcSDK + store: root.dappsStore + supportedAccountsModel: root.accountsModel + onConnected: (proposalId, topic, dappUrl) => { + root.connected(proposalId, topic, dappUrl, Constants.WalletConnect) + } + onDisconnected: (topic, dappUrl) => { + root.disconnected(topic, dappUrl) + } + } + + BCDappsProvider { + id: connectorDAppsProvider + bcSDK: root.bcSDK + onConnected: (pairingId, topic, dappUrl) => { + root.connected(pairingId, topic, dappUrl, Constants.StatusConnect) + } + onDisconnected: (topic, dappUrl) => { + root.disconnected(topic, dappUrl) + } + } + + ConcatModel { + id: dappsModel + markerRoleName: "source" + + sources: [ + SourceModel { + model: dappsProvider.model + markerRoleValue: "walletConnect" + }, + SourceModel { + model: connectorDAppsProvider.model + markerRoleValue: "statusConnect" + } + ] + } + + // These two objects don't share a common interface because Qt5.15.2 would freeze for some reason + QtObject { + id: bcConnectionPromise + function resolve(context, key, approvedChainIds, accountAddress) { + root.bcSDK.approveSession(key, accountAddress, approvedChainIds) + } + function reject(context, key) { + root.bcSDK.rejectSession(key) + } + } + + QtObject { + id: wcConnectionPromise + function resolve(context, key, approvedChainIds, accountAddress) { + const approvedNamespaces = JSON.parse( + DAppsHelpers.buildSupportedNamespaces(approvedChainIds, + [accountAddress], + SessionRequest.getSupportedMethods())) + d.acceptedSessionProposal = context + d.acceptedNamespaces = approvedNamespaces + + root.wcSDK.buildApprovedNamespaces(key, context.params, approvedNamespaces) + } + function reject(context, key) { + root.wcSDK.rejectSession(key) + } + } + + // Flow for BrowserConnect + // 1. onSessionProposal -> new connection proposal received + // 3. onApproveSessionResult -> session approve result + // 4. onRejectSessionResult -> session reject result + Connections { + target: root.bcSDK + + function onSessionProposal(sessionProposal) { + const key = sessionProposal.id + d.activeProposals.set(key.toString(), { context: sessionProposal, promise: bcConnectionPromise }) + root.newConnectionProposed(key, [1], sessionProposal.params.proposer.metadata.url, sessionProposal.params.proposer.metadata.name, sessionProposal.params.proposer.metadata.icons[0]) + } + + function onApproveSessionResult(proposalId, session, err) { + if (!d.activeProposals.has(proposalId.toString())) { + console.error("No active proposal found for key: " + proposalId) + return + } + const dappUrl = d.activeProposals.get(proposalId.toString()).context.params.proposer.metadata.url + d.activeProposals.delete(proposalId.toString()) + if (err) { + root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.unknownError) + return + } + } + + function onRejectSessionResult(proposalId, err) { + if (!d.activeProposals.has(proposalId.toString())) { + console.error("No active proposal found for key: " + proposalId) + return + } + const dappUrl = d.activeProposals.get(proposalId.toString()).context.params.proposer.metadata.url + d.activeProposals.delete(proposalId.toString()) + if (err) { + root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.rejectFailed) + return + } + root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.userRejected) + } + } + + // Flow for WalletConnect + // 1. onSessionProposal -> new connection proposal received + // 2. onBuildApprovedNamespacesResult -> get the supported namespaces to be sent for approval + // 3. onApproveSessionResult -> session approve result + // 4. onRejectSessionResult -> session reject result + Connections { + target: root.wcSDK + + function onSessionProposal(sessionProposal) { + const key = sessionProposal.id + d.activeProposals.set(key.toString(), { context: sessionProposal, promise: wcConnectionPromise }) + const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels( + root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods()) + root.wcSDK.buildApprovedNamespaces(key, sessionProposal.params, JSON.parse(supportedNamespacesStr)) + } + + function onBuildApprovedNamespacesResult(key, approvedNamespaces, error) { + if (!d.activeProposals.has(key.toString())) { + console.error("No active proposal found for key: " + key) + return + } + const proposal = d.activeProposals.get(key.toString()).context + const dAppUrl = proposal.params.proposer.metadata.url + + if(error || !approvedNamespaces) { + // Check that it contains Non conforming namespaces" + if (error.includes("Non conforming namespaces")) { + root.newConnectionFailed(proposal.id, dAppUrl, Pairing.errors.unsupportedNetwork) + } else { + root.newConnectionFailed(proposal.id, dAppUrl, Pairing.errors.unknownError) + } + return + } + + approvedNamespaces = d.applyChainAgnosticFix(approvedNamespaces) + if (d.acceptedSessionProposal) { + root.wcSDK.approveSession(d.acceptedSessionProposal, approvedNamespaces) + } else { + const res = DAppsHelpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces) + const chains = res.chains + const dAppName = proposal.params.proposer.metadata.name + const dAppIcons = proposal.params.proposer.metadata.icons + const dAppIcon = dAppIcons && dAppIcons.length > 0 ? dAppIcons[0] : "" + + root.newConnectionProposed(key, chains, dAppUrl, dAppName, dAppIcon) + } + } + + function onApproveSessionResult(proposalId, session, err) { + if (!d.activeProposals.has(proposalId.toString())) { + console.error("No active proposal found for key: " + proposalId) + return + } + const dappUrl = d.activeProposals.get(proposalId.toString()) + .context.params.proposer.metadata.url + d.activeProposals.delete(proposalId.toString()) + d.acceptedSessionProposal = null + d.acceptedNamespaces = null + if (err) { + root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.unknownError) + return + } + } + + function onRejectSessionResult(proposalId, err) { + if (!d.activeProposals.has(proposalId.toString())) { + console.error("No active proposal found for key: " + proposalId) + return + } + const dappUrl = d.activeProposals.get(proposalId.toString()) + .context.params.proposer.metadata.url + d.activeProposals.delete(proposalId.toString()) + if (err) { + root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.rejectFailed) + return + } + root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.userRejected) + } + } + + QtObject { + id: d + + property var activeProposals: new Map() + property var acceptedSessionProposal: null + property var acceptedNamespaces: null + + function disconnect(connectionId) { + const dApp = d.getDAppByTopic(connectionId) + if (!dApp) { + console.error("Disconnecting dApp: dApp not found") + return + } + if (!dApp.connectorId == undefined) { + console.error("Disconnecting dApp: connectorId not found") + return + } + + const sdk = dApp.connectorId === Constants.WalletConnect ? root.wcSDK : root.bcSDK + sdkDisconnect(dApp, sdk) + } + + // Disconnect all sessions for a dApp + function sdkDisconnect(dapp, sdk) { + SQUtils.ModelUtils.forEach(dapp.rawSessions, (session) => { + sdk.disconnectSession(session.topic) + }) + } + + function reject(key) { + if (!d.activeProposals.has(key.toString())) { + console.error("Rejecting dApp: dApp not found") + return + } + + const proposal = d.activeProposals.get(key.toString()) + proposal.promise.reject(proposal.context, key) + } + + function connect(key, chains, accoutAddress) { + if (!d.activeProposals.has(key.toString())) { + console.error("Connecting dApp: dApp not found", key) + return + } + + const proposal = d.activeProposals.get(key.toString()) + proposal.promise.resolve(proposal.context, key, chains, accoutAddress) + } + + function getDAppByTopic(topic) { + return SQUtils.ModelUtils.getFirstModelEntryIf(dappsModel, (modelItem) => { + if (modelItem.topic == topic) { + return true + } + if (!modelItem.rawSessions) { + return false + } + for (let i = 0; i < modelItem.rawSessions.ModelCount.count; i++) { + if (modelItem.rawSessions.get(i).topic == topic) { + return true + } + } + }) + } + + //Special case for chain agnostic dapps + //WC considers the approved namespace as valid, but there's no chainId or account established + //Usually this request is declared by using `eip155:0`, but we don't support this chainID, resulting in empty `chains` and `accounts` + //The established connection will use for all user approved chains and accounts + //This fix is applied to all valid namespaces that don't have a chainId or account + function applyChainAgnosticFix(approvedNamespaces) { + try { + const an = approvedNamespaces.eip155 + const chainAgnosticRequest = (!an.chains || an.chains.length === 0) && (!an.accounts || an.accounts.length === 0) + if (!chainAgnosticRequest) { + return approvedNamespaces + } + + // If the `d.acceptedNamespaces` is set it means the user already confirmed the chain and account + if (!!d.acceptedNamespaces) { + approvedNamespaces.eip155.chains = d.acceptedNamespaces.eip155.chains + approvedNamespaces.eip155.accounts = d.acceptedNamespaces.eip155.accounts + return approvedNamespaces + } + + // Show to the user all possible chains + const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels( + root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods()) + const supportedNamespaces = JSON.parse(supportedNamespacesStr) + + approvedNamespaces.eip155.chains = supportedNamespaces.eip155.chains + approvedNamespaces.eip155.accounts = supportedNamespaces.eip155.accounts + } catch (e) { + console.warn("WC Error applying chain agnostic fix", e) + } + + return approvedNamespaces + } + } +} \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/plugins/SignRequestPlugin.qml b/ui/app/AppLayouts/Wallet/services/dapps/plugins/SignRequestPlugin.qml new file mode 100644 index 0000000000..1b934185a5 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/services/dapps/plugins/SignRequestPlugin.qml @@ -0,0 +1,671 @@ +import QtQuick 2.15 +import QtQml 2.15 + +import AppLayouts.Wallet.services.dapps 1.0 +import AppLayouts.Wallet.services.dapps.types 1.0 + +import StatusQ 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils + +import shared.stores 1.0 +import utils 1.0 + +/// Plugin that listens for session requests and manages the lifecycle of the request. +SQUtils.QObject { + id: root + + required property WalletConnectSDKBase sdk + required property DAppsStore store + /// Expected to have the following roles: + /// - topic + /// - name + /// - url + /// - iconUrl + /// - rawSessions + required property var dappsModel + /// Expected to have the following roles: + /// - tokensKey + /// - balances + required property var groupedAccountAssetsModel + /// Expected to have the following roles: + /// - layer + /// - chainId + required property var networksModel + /// Expected to have the following roles: + /// - address + required property var accountsModel + /// App currency + required property string currentCurrency + // SessionRequestsModel where the requests are stored + // This component will append and remove requests from this model + required property SessionRequestsModel requests + // Function to transform the eth value to fiat + property var getFiatValue: (maxFeesEthStr, token /*Constants.ethToken*/) => console.error("getFiatValue not implemented") + + // Signals + /// Signal emitted when a session request is accepted + signal accepted(string topic, string id, string data) + /// Signal emitted when a session request is rejected + signal rejected(string topic, string id, bool hasError) + /// Signal emitted when a session request is completed + /// Completed mean that we have the ACK from the SDK + signal signCompleted(string topic, string id, bool userAccepted, string error) + + function requestReceived(event, dappName, dappUrl, dappIcon, connectorId) { + d.onSessionRequestEvent(event, dappName, dappUrl, dappIcon, connectorId) + } + + function requestResolved(topic, id) { + root.requests.removeRequest(topic, id) + } + + function requestExpired(sessionId) { + d.onSessionRequestExpired(sessionId) + } + + onRejected: (topic, id, hasError) => { + root.sdk.rejectSessionRequest(topic, id, hasError) + } + + onAccepted: (topic, id, data) => { + root.sdk.acceptSessionRequest(topic, id, data) + } + + Component { + id: sessionRequestComponent + + SessionRequestWithAuth { + id: request + store: root.store + + function signedHandler(topic, id, data) { + if (topic != request.topic || id != request.requestId) { + return + } + root.store.signingResult.disconnect(request.signedHandler) + + let hasErrors = (data == "") + if (!hasErrors) { + root.accepted(topic, id, data) + } else { + request.reject(true) + } + } + + onActiveChanged: { + if (active === false) { + d.unsubscribeForFeeUpdates(request.topic, request.requestId) + } + if (active === true) { + d.subscribeForFeeUpdates(request.topic, request.requestId) + } + } + + onRejected: (hasError) => { + root.rejected(request.topic, request.requestId, hasError) + d.unsubscribeForFeeUpdates(request.topic, request.requestId) + } + + onAuthFailed: () => { + root.rejected(request.topic, request.requestId, true /*hasError*/) + d.unsubscribeForFeeUpdates(request.topic, request.requestId) + } + + onExecute: (password, pin) => { + d.unsubscribeForFeeUpdates(request.topic, request.requestId) + root.store.signingResult.connect(request.signedHandler) + let executed = false + try { + executed = d.executeSessionRequest(request, password, pin, request.feesInfo) + } catch (e) { + console.error("Error executing session request", e) + } + + if (!executed) { + root.rejected(request.topic, request.requestId, true /*hasError*/) + root.store.signingResult.disconnect(request.signedHandler) + } + } + } + } + + Connections { + target: root.sdk + + function onSessionRequestEvent(sessionRequest) { + const { id, topic } = sessionRequest + const dapp = SQUtils.ModelUtils.getFirstModelEntryIf(root.dappsModel, (dapp) => { + if (dapp.topic === topic) { + return true + } + return !!SQUtils.ModelUtils.getFirstModelEntryIf(dapp.rawSessions, (session) => { + if (session.topic === topic) { + return true + } + }) + }) + + if (!dapp) { + console.warn("Error finding dapp for topic", topic, "id", id) + root.sdk.rejectSessionRequest(topic, id, true) + return + } + + root.requestReceived(sessionRequest, dapp.name, dapp.url, dapp.iconUrl, dapp.connectorId) + } + + function onSessionRequestExpired(sessionId) { + root.requestExpired(sessionId) + } + + function onSessionRequestUserAnswerResult(topic, id, accept, error) { + let request = root.requests.findRequest(topic, id) + + if (request === null) { + console.error("Error finding event for topic", topic, "id", id) + return + } + Qt.callLater(() => root.requestResolved(topic, id)) + + if (error) { + root.signCompleted(topic, id, accept, error) + console.error(`Error accepting session request for topic: ${topic}, id: ${id}, accept: ${accept}, error: ${error}`) + return + } + + root.signCompleted(topic, id, accept, "") + } + } + + QtObject { + id: d + + function onSessionRequestEvent(event, dappName, dappUrl, dappIcon, connectorId) { + try { + const res = d.resolve(event, dappName, dappUrl, dappIcon, connectorId) + if (res.conde === SessionRequest.Ignored) { + return + } + if (res.code !== SessionRequest.NoError) { + root.rejected(event.topic, event.id, true) + return + } + root.requests.enqueue(res.obj) + } catch (e) { + console.error("Error processing session request event", e) + root.rejected(event.topic, event.id, true) + } + } + + function onSessionRequestExpired(sessionId) { + // Expired event coming from WC + // Handling as a failsafe in case the event is not processed by the SDK + let request = root.requests.findById(sessionId) + if (request === null) { + console.error("Error finding event for session id", sessionId) + return + } + + if (request.isExpired()) { + return //nothing to do. The request is already expired + } + + request.setExpired() + d.unsubscribeForFeeUpdates(request.topic, request.requestId) + } + // returns { + // obj: obj or nil + // code: SessionRequest.ErrorCode + // } + function resolve(event, dappName, dappUrl, dappIcon, connectorId) { + const {request, error} = SessionRequestResolver.resolveEvent(event, root.accountsModel, root.networksModel, root.store.hexToDec) + if (error !== SessionRequest.NoError) { + return { obj: null, code: error } + } + if (!request) { + return { obj: null, code: SessionRequest.RuntimeError } + } + const mainNet = lookupMainnetNetwork() + if (!mainNet) { + console.error("Mainnet network not found") + return { obj: null, code: SessionRequest.RuntimeError } + } + + updateFeesOnPreparedData(request) + + let obj = sessionRequestComponent.createObject(null, { + event: request.event, + topic: request.topic, + requestId: request.requestId, + method: request.method, + accountAddress: request.account, + chainId: request.chainId, + data: request.data, + preparedData: JSON.stringify(request.preparedData), + expirationTimestamp: request.expiryTimestamp, + dappName, + dappUrl, + dappIcon, + sourceId: connectorId, + value: request.value + }) + if (obj === null) { + console.error("Error creating SessionRequestResolved for event") + return { obj: null, code: SessionRequest.RuntimeError } + } + + if (!request.transaction) { + obj.haveEnoughFunds = true + return { obj: obj, code: SessionRequest.NoError } + } + + updateFeesParamsToPassedObj(obj) + + return { + obj: obj, + code: SessionRequest.NoError + } + } + + + // Updates the fees to a SessionRequestResolved + function updateFeesParamsToPassedObj(requestItem) { + if (!(requestItem instanceof SessionRequestResolved)) { + return + } + if (!SessionRequest.isTransactionMethod(requestItem.method)) { + return + } + + const mainNet = lookupMainnetNetwork() + if (!mainNet) { + console.error("Mainnet network not found") + return { obj: null, code: SessionRequest.RuntimeError } + } + + const tx = SessionRequest.getTxObject(requestItem.method, requestItem.data) + requestItem.estimatedTimeCategory = root.store.getEstimatedTime(requestItem.chainId, tx.maxFeePerGas || tx.gasPrice || "") + + let st = getEstimatedFeesStatus(tx, requestItem.method, requestItem.chainId, mainNet.chainId) + let fundsStatus = checkFundsStatus(st.feesInfo.maxFees, st.feesInfo.l1GasFee, requestItem.accountAddress, requestItem.chainId, mainNet.chainId, requestItem.value) + requestItem.fiatMaxFees = st.fiatMaxFees + requestItem.ethMaxFees = st.maxFeesEth + requestItem.haveEnoughFunds = fundsStatus.haveEnoughFunds + requestItem.haveEnoughFees = fundsStatus.haveEnoughForFees + requestItem.feesInfo = st.feesInfo + } + + // Updates the fee in the transaction preview on a JS Object built by SessionRequest + function updateFeesOnPreparedData(request) { + if (!request.transaction && !request.preparedData instanceof Object) { + return + } + + let fees = root.store.getSuggestedFees(request.chainId) + if (!request.preparedData.maxFeePerGas + && request.preparedData.hasOwnProperty("maxFeePerGas") + && fees.eip1559Enabled) { + request.preparedData.maxFeePerGas = d.getFeesForFeesMode(fees) + } + + if (!request.preparedData.maxPriorityFeePerGas + && request.preparedData.hasOwnProperty("maxPriorityFeePerGas") + && fees.eip1559Enabled) { + request.preparedData.maxPriorityFeePerGas = fees.maxPriorityFeePerGas + } + + if (!request.preparedData.gasPrice + && request.preparedData.hasOwnProperty("gasPrice") + && !fees.eip1559Enabled) { + request.preparedData.gasPrice = fees.gasPrice + } + } + + /// Returns null if the network is not found + function lookupMainnetNetwork() { + return SQUtils.ModelUtils.getByKey(root.networksModel, "layer", 1) + } + + function executeSessionRequest(request, password, pin, payload) { + if (!SessionRequest.getSupportedMethods().includes(request.method)) { + console.error("Unsupported method to execute: ", request.method) + return false + } + + if (password === "") { + console.error("No password provided to sign message") + return false + } + + if (request.method === SessionRequest.methods.sign.name) { + root.store.signMessageUnsafe(request.topic, + request.requestId, + request.accountAddress, + SessionRequest.methods.personalSign.getMessageFromData(request.data), + password, + pin) + } else if (request.method === SessionRequest.methods.personalSign.name) { + root.store.signMessage(request.topic, + request.requestId, + request.accountAddress, + SessionRequest.methods.personalSign.getMessageFromData(request.data), + password, + pin) + } else if (request.method === SessionRequest.methods.signTypedData_v4.name || + request.method === SessionRequest.methods.signTypedData.name) + { + let legacy = request.method === SessionRequest.methods.signTypedData.name + root.store.safeSignTypedData(request.topic, + request.requestId, + request.accountAddress, + SessionRequest.methods.signTypedData.getMessageFromData(request.data), + request.chainId, + legacy, + password, + pin) + } else if (SessionRequest.isTransactionMethod(request.method)) { + let txObj = SessionRequest.getTxObject(request.method, request.data) + if (!!payload) { + let hexFeesJson = root.store.convertFeesInfoToHex(JSON.stringify(payload)) + if (!!hexFeesJson) { + let feesInfo = JSON.parse(hexFeesJson) + if (feesInfo.maxFeePerGas) { + txObj.maxFeePerGas = feesInfo.maxFeePerGas + } + if (feesInfo.maxPriorityFeePerGas) { + txObj.maxPriorityFeePerGas = feesInfo.maxPriorityFeePerGas + } + } + delete txObj.gasLimit + delete txObj.gasPrice + } + // Remove nonce from txObj to be auto-filled by the wallet + delete txObj.nonce + + if (request.method === SessionRequest.methods.signTransaction.name) { + root.store.signTransaction(request.topic, + request.requestId, + request.accountAddress, + request.chainId, + txObj, + password, + pin) + } else if (request.method === SessionRequest.methods.sendTransaction.name) { + root.store.sendTransaction( + request.topic, + request.requestId, + request.accountAddress, + request.chainId, + txObj, + password, + pin) + } + } + + return true + } + + // Returns { + // maxFees -> Big number in Gwei + // maxFeePerGas + // maxPriorityFeePerGas + // gasPrice + // } + function getEstimatedMaxFees(tx, method, chainId, mainNetChainId) { + const BigOps = SQUtils.AmountsArithmetic + const gasLimit = BigOps.fromString("21000") + const parsedTransaction = SessionRequest.parseTransaction(tx, root.store.hexToDec) + let gasPrice = BigOps.fromString(parsedTransaction.maxFeePerGas) + let maxFeePerGas = BigOps.fromString(parsedTransaction.maxFeePerGas) + let maxPriorityFeePerGas = BigOps.fromString(parsedTransaction.maxPriorityFeePerGas) + let l1GasFee = BigOps.fromNumber(0) + + if (!maxFeePerGas || !maxPriorityFeePerGas || !gasPrice) { + const suggesteFees = getSuggestedFees(chainId) + maxFeePerGas = suggesteFees.maxFeePerGas + maxPriorityFeePerGas = suggesteFees.maxPriorityFeePerGas + gasPrice = suggesteFees.gasPrice + l1GasFee = suggesteFees.l1GasFee + } + + let maxFees = BigOps.times(gasLimit, gasPrice) + return {maxFees, maxFeePerGas, maxPriorityFeePerGas, gasPrice, l1GasFee} + } + + function getSuggestedFees(chainId) { + const BigOps = SQUtils.AmountsArithmetic + const fees = root.store.getSuggestedFees(chainId) + const maxPriorityFeePerGas = fees.maxPriorityFeePerGas + let maxFeePerGas + let gasPrice + if (fees.eip1559Enabled) { + if (!!fees.maxFeePerGasM) { + gasPrice = BigOps.fromNumber(fees.maxFeePerGasM) + maxFeePerGas = fees.maxFeePerGasM + } else if(!!tx.maxFeePerGas) { + let maxFeePerGasDec = root.store.hexToDec(tx.maxFeePerGas) + gasPrice = BigOps.fromString(maxFeePerGasDec) + maxFeePerGas = maxFeePerGasDec + } else { + console.error("Error fetching maxFeePerGas from fees or tx objects") + return + } + } else { + if (!!fees.gasPrice) { + gasPrice = BigOps.fromNumber(fees.gasPrice) + } else { + console.error("Error fetching suggested fees") + return + } + } + const l1GasFee = BigOps.fromNumber(fees.l1GasFee) + return {maxFeePerGas, maxPriorityFeePerGas, gasPrice, l1GasFee} + } + + // Returned values are Big numbers + function getEstimatedFeesStatus(tx, method, chainId, mainNetChainId) { + const BigOps = SQUtils.AmountsArithmetic + + const feesInfo = getEstimatedMaxFees(tx, method, chainId, mainNetChainId) + + const totalMaxFees = BigOps.sum(feesInfo.maxFees, feesInfo.l1GasFee) + const maxFeesEth = BigOps.div(totalMaxFees, BigOps.fromNumber(1, 9)) + + const maxFeesEthStr = maxFeesEth.toString() + const fiatMaxFeesStr = root.getFiatValue(maxFeesEthStr, Constants.ethToken) + const fiatMaxFees = BigOps.fromString(fiatMaxFeesStr) + const symbol = root.currentCurrency + + return {fiatMaxFees, maxFeesEth, symbol, feesInfo} + } + + function getBalanceInEth(balances, address, chainId) { + const BigOps = SQUtils.AmountsArithmetic + const accEth = SQUtils.ModelUtils.getFirstModelEntryIf(balances, (balance) => { + return balance.account.toLowerCase() === address.toLowerCase() && balance.chainId == chainId + }) + if (!accEth) { + console.error("Error balance lookup for account ", address, " on chain ", chainId) + return null + } + const accountFundsWei = BigOps.fromString(accEth.balance) + return BigOps.div(accountFundsWei, BigOps.fromNumber(1, 18)) + } + + // Returns {haveEnoughForFees, haveEnoughFunds} and true in case of error not to block request + function checkFundsStatus(maxFees, l1GasFee, address, chainId, mainNetChainId, value) { + const BigOps = SQUtils.AmountsArithmetic + let valueEth = BigOps.fromString(value) + let haveEnoughForFees = true + let haveEnoughFunds = true + + let token = SQUtils.ModelUtils.getByKey(root.groupedAccountAssetsModel, "tokensKey", Constants.ethToken) + if (!token || !token.balances) { + console.error("Error token balances lookup for ETH", SQUtils.ModelUtils.modelToArray(root.groupedAccountAssetsModel)) + console.error("Looking for tokensKey: ", Constants.ethToken) + return {haveEnoughForFees, haveEnoughFunds} + } + + let chainBalance = getBalanceInEth(token.balances, address, chainId) + if (!chainBalance) { + console.error("Error fetching chain balance") + return {haveEnoughForFees, haveEnoughFunds} + } + haveEnoughFunds = BigOps.cmp(chainBalance, valueEth) >= 0 + if (haveEnoughFunds) { + chainBalance = BigOps.sub(chainBalance, valueEth) + + if (chainId == mainNetChainId) { + const finalFees = BigOps.sum(maxFees, l1GasFee) + let feesEth = BigOps.div(finalFees, BigOps.fromNumber(1, 9)) + haveEnoughForFees = BigOps.cmp(chainBalance, feesEth) >= 0 + } else { + const feesChain = BigOps.div(maxFees, BigOps.fromNumber(1, 9)) + const haveEnoughOnChain = BigOps.cmp(chainBalance, feesChain) >= 0 + + const mainBalance = getBalanceInEth(token.balances, address, mainNetChainId) + if (!mainBalance) { + console.error("Error fetching mainnet balance") + return {haveEnoughForFees, haveEnoughFunds} + } + const feesMain = BigOps.div(l1GasFee, BigOps.fromNumber(1, 9)) + const haveEnoughOnMain = BigOps.cmp(mainBalance, feesMain) >= 0 + + haveEnoughForFees = haveEnoughOnChain && haveEnoughOnMain + } + } else { + haveEnoughForFees = false + } + + return {haveEnoughForFees, haveEnoughFunds} + } + + property int selectedFeesMode: Constants.FeesMode.Medium + + function getFeesForFeesMode(feesObj) { + if (!(feesObj.hasOwnProperty("maxFeePerGasL") && + feesObj.hasOwnProperty("maxFeePerGasM") && + feesObj.hasOwnProperty("maxFeePerGasH"))) { + throw new Error("inappropriate fees object provided") + } + + switch (d.selectedFeesMode) { + case Constants.FeesMode.Low: + return feesObj.maxFeePerGasL + case Constants.FeesMode.Medium: + return feesObj.maxFeePerGasM + case Constants.FeesMode.High: + return feesObj.maxFeePerGasH + default: + throw new Error("unknown selected mode") + } + } + + property var feesSubscriptions: [] + + function findSubscriptionIndex(topic, id) { + for (let i = 0; i < d.feesSubscriptions.length; i++) { + const subscription = d.feesSubscriptions[i] + if (subscription.topic == topic && subscription.id == id) { + return i + } + } + return -1 + } + + function findChainIndex(chainId) { + for (let i = 0; i < feesSubscription.chainIds.length; i++) { + if (feesSubscription.chainIds[i] == chainId) { + return i + } + } + return -1 + } + + function subscribeForFeeUpdates(topic, id) { + const request = requests.findRequest(topic, id) + if (request === null) { + console.error("Error finding event for subscribing for fees for topic", topic, "id", id) + return + } + + const index = d.findSubscriptionIndex(topic, id) + if (index >= 0) { + return + } + + d.feesSubscriptions.push({ + topic: topic, + id: id, + chainId: request.chainId + }) + + for (let i = 0; i < feesSubscription.chainIds.length; i++) { + if (feesSubscription.chainIds == request.chainId) { + return + } + } + + feesSubscription.chainIds.push(request.chainId) + feesSubscription.restart() + } + + function unsubscribeForFeeUpdates(topic, id) { + const index = d.findSubscriptionIndex(topic, id) + if (index == -1) { + return + } + + const chainId = d.feesSubscriptions[index].chainId + d.feesSubscriptions.splice(index, 1) + + const chainIndex = d.findChainIndex(chainId) + if (index == -1) { + return + } + + let found = false + for (let i = 0; i < d.feesSubscriptions.length; i++) { + if (d.feesSubscriptions[i].chainId == chainId) { + found = true + break + } + } + + if (found) { + return + } + + feesSubscription.chainIds.splice(chainIndex, 1) + if (feesSubscription.chainIds.length == 0) { + feesSubscription.stop() + } + } + } + + Timer { + id: feesSubscription + + property var chainIds: [] + + interval: 5000 + repeat: true + running: Qt.application.state === Qt.ApplicationActive + + onTriggered: { + for (let i = 0; i < chainIds.length; i++) { + for (let j = 0; j < d.feesSubscriptions.length; j++) { + let subscription = d.feesSubscriptions[j] + if (subscription.chainId == chainIds[i]) { + let request = requests.findRequest(subscription.topic, subscription.id) + if (request === null) { + console.error("Error updating fees for topic", subscription.topic, "id", subscription.id) + continue + } + d.updateFeesParamsToPassedObj(request) + } + } + } + } + } +} \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/plugins/SiweLifeCycle.qml b/ui/app/AppLayouts/Wallet/services/dapps/plugins/SiweLifeCycle.qml index 822b55aa72..8e154466da 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/plugins/SiweLifeCycle.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/plugins/SiweLifeCycle.qml @@ -174,8 +174,6 @@ SQUtils.QObject { topic: root.request.topic data: "" preparedData: "" - maxFeesText: "?" - maxFeesEthText: "?" expirationTimestamp: root.request.params.expiryTimestamp function onBuildAuthenticationObjectResult(id, authObject, error) { @@ -232,6 +230,7 @@ SQUtils.QObject { onAuthFailed: () => { try { + sdk.rejectSessionAuthenticate(request.requestId, true) const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl) const methodStr = SessionRequest.methodToUserString(request.method) if (!methodStr) { diff --git a/ui/app/AppLayouts/Wallet/services/dapps/plugins/SiweRequestPlugin.qml b/ui/app/AppLayouts/Wallet/services/dapps/plugins/SiweRequestPlugin.qml index dc02681ed7..9ee9d0fbf3 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/plugins/SiweRequestPlugin.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/plugins/SiweRequestPlugin.qml @@ -22,6 +22,8 @@ SQUtils.QObject { // - chainId required property var networksModel + readonly property bool enabled: sdk.enabled + // Trigger a connection request to the dApp // Expected `connectionApproved` to be called with the key and the approved namespaces signal connectDApp(var chains, string dAppUrl, string dAppName, string dAppIcon, var key) @@ -109,6 +111,7 @@ SQUtils.QObject { Connections { target: sdk + enabled: root.enabled function onSessionAuthenticateRequest(sessionData) { if (!sessionData || !sessionData.id) { diff --git a/ui/app/AppLayouts/Wallet/services/dapps/plugins/qmldir b/ui/app/AppLayouts/Wallet/services/dapps/plugins/qmldir index b9a33432d0..424d70b7fc 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/plugins/qmldir +++ b/ui/app/AppLayouts/Wallet/services/dapps/plugins/qmldir @@ -1,3 +1,5 @@ ChainsSupervisorPlugin 1.0 ChainsSupervisorPlugin.qml +DAppConnectionsPlugin 1.0 DAppConnectionsPlugin.qml SiweRequestPlugin 1.0 SiweRequestPlugin.qml -SiweLifeCycle 1.0 SiweLifeCycle.qml \ No newline at end of file +SiweLifeCycle 1.0 SiweLifeCycle.qml +SignRequestPlugin 1.0 SignRequestPlugin.qml \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/qmldir b/ui/app/AppLayouts/Wallet/services/dapps/qmldir index 6735eff896..44d0300cc9 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/qmldir +++ b/ui/app/AppLayouts/Wallet/services/dapps/qmldir @@ -2,8 +2,8 @@ BCDappsProvider 1.0 BCDappsProvider.qml DappsConnectorSDK 1.0 DappsConnectorSDK.qml DAppsHelpers 1.0 helpers.js DAppsModel 1.0 DAppsModel.qml -DAppsRequestHandler 1.0 DAppsRequestHandler.qml +DAppsModule 1.0 DAppsModule.qml +DAppsService 1.0 DAppsService.qml WalletConnectSDKBase 1.0 WalletConnectSDKBase.qml WalletConnectSDK 1.0 WalletConnectSDK.qml -WalletConnectService 1.0 WalletConnectService.qml WCDappsProvider 1.0 WCDappsProvider.qml diff --git a/ui/app/AppLayouts/Wallet/services/dapps/types/Pairing.qml b/ui/app/AppLayouts/Wallet/services/dapps/types/Pairing.qml index 0cd4568cd4..03a65c3ae9 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/types/Pairing.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/types/Pairing.qml @@ -13,5 +13,7 @@ QtObject { readonly property int unsupportedNetwork: 6 readonly property int unknownError: 7 readonly property int dappReadyForApproval: 8 + readonly property int userRejected: 9 + readonly property int rejectFailed: 10 } } \ 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 e644aa38d2..c1e492ff3f 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequest.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequest.qml @@ -2,6 +2,8 @@ pragma Singleton import QtQml 2.15 +import AppLayouts.Wallet.services.dapps 1.0 +import StatusQ.Core.Utils 0.1 as SQUtils import utils 1.0 QtObject { @@ -62,6 +64,93 @@ QtObject { readonly property var all: [personalSign, sign, signTypedData_v4, signTypedData, sendTransaction] } + enum ErrorCode { + NoError, + InvalidAccount, + InvalidChainId, + InvalidData, + InvalidMessage, + InvalidMethod, + UnsupportedMethod, + InvalidRequest, + RuntimeError, + Ignored + } + /* + Function parsing the session request event + Throws exception if the event is invalid + returns { request: [Object], error: [ErrorCode] } + request: { + event, + topic, + requestId, + method, + account, + chainId, + data, + preparedData, + expiryTimestamp, + transaction: { + value, + maxFeePerGas, + maxPriorityFeePerGas, + gasPrice, + gasLimit, + nonce + } + } + */ + function parse(event, hexToDec) { + if (!event) { + console.warn("SessionRequest - parse - invalid event") + return { request: null, error: SessionRequest.InvalidRequest } + } + if (!hexToDec) { + hexToDec = (hex) => { return parseInt(hex, 16) } + } + let request = {} + let error = SessionRequest.NoError + + request.event = event + request.topic = event.topic + request.requestId = event.id + request.method = event.params.request.method + if (!request.method) { + console.warn("SessionRequest - build - invalid method") + return { request: null, error: SessionRequest.InvalidMethod } + } + if (getSupportedMethods().includes(request.method) === false) { + console.warn("Unsupported method:", request.method) + return { request: null, error: SessionRequest.UnsupportedMethod } + } + const { accountAddress, success } = getAccountFromEvent(event) + if (!accountAddress || !success) { + console.warn("SessionRequest - build - failed to get account from event") + return { request: null, error: SessionRequest.InvalidAccount } + } + request.account = accountAddress + request.data = getData(event) + if (!request.data) { + console.warn("SessionRequest - build - failed to get data from event") + return { request: null, error: SessionRequest.InvalidData } + } + const message = getMessage(event, hexToDec) + if (!message || !message.signData) { + console.warn("SessionRequest - build - failed to get message from event") + return { request: null, error: SessionRequest.InvalidMessage } + } + request.preparedData = message.signData + request.value = message.value + request.transaction = message.transaction + request.chainId = getChainId(event) + if (!request.chainId) { + console.warn("SessionRequest - build - failed to get chainId from event") + return { request: null, error: SessionRequest.InvalidChainId } + } + request.expiryTimestamp = getExpiryDate(event) + return { request, error} + } + function getSupportedMethods() { return methods.all.map(function(method) { return method.name @@ -76,4 +165,228 @@ QtObject { } return "" } + + /// returns { + /// accountAddress + /// success + /// } + /// if account is null and success is true it means that the account was not found + function getAccountFromEvent(event) { + const method = event.params.request.method + let address = "" + if (method === methods.personalSign.name) { + if (event.params.request.params.length < 2) { + return { accountAddress: "", success: false } + } + address = event.params.request.params[1] + } else if (method === methods.sign.name) { + if (event.params.request.params.length === 1) { + return { accountAddress: "", success: false } + } + address = event.params.request.params[0] + } else if(method === methods.signTypedData_v4.name || + method === methods.signTypedData.name) + { + if (event.params.request.params.length < 2) { + return { accountAddress: "", success: false } + } + address = event.params.request.params[0] + } else if (isTransactionMethod(method)) { + if (event.params.request.params.length == 0) { + return { accountAddress: "", success: false } + } + address = event.params.request.params[0].from + } else { + console.error("Unsupported method to lookup account: ", method) + return { accountAddress: "", success: false } + } + return { accountAddress: address, success: true } + } + + function getChainId(event) { + return DAppsHelpers.chainIdFromEip155(event.params.chainId) + } + + function getData(event) { + const method = event.params.request.method + if (method === methods.personalSign.name || + method === methods.sign.name) + { + if (event.params.request.params.length < 1) { + return null + } + let message = "" + const messageIndex = (method === methods.personalSign.name ? 0 : 1) + const messageParam = event.params.request.params[messageIndex] + // There is no standard on how data is encoded. Therefore we support hex or utf8 + if (DAppsHelpers.isHex(messageParam)) { + message = DAppsHelpers.hexToString(messageParam) + } else { + message = messageParam + } + return methods.personalSign.buildDataObject(message) + } else if (method === methods.signTypedData_v4.name || + method === methods.signTypedData.name) + { + if (event.params.request.params.length < 2) { + return null + } + const jsonMessage = event.params.request.params[1] + const methodObj = method === methods.signTypedData_v4.name + ? methods.signTypedData_v4 + : methods.signTypedData + return methodObj.buildDataObject(jsonMessage) + } else if (method === methods.signTransaction.name) { + if (event.params.request.params.length == 0) { + return null + } + const tx = event.params.request.params[0] + return methods.signTransaction.buildDataObject(tx) + } else if (method === methods.sendTransaction.name) { + if (event.params.request.params.length == 0) { + return null + } + const tx = event.params.request.params[0] + return methods.sendTransaction.buildDataObject(tx) + } else { + return null + } + } + + // returns { + // signData, + // transaction: { + // value, + // maxFeePerGas, + // maxPriorityFeePerGas, + // gasPrice, + // gasLimit, + // nonce + // }, + // value // null or ETH Big number + // } + function getMessage(event, hexToDec) { + const data = getData(event) + const method = event.params.request.method + return prepareData(method, data, hexToDec) + } + + // returns { + // signData, + // transaction: { + // value, + // maxFeePerGas, + // maxPriorityFeePerGas, + // gasPrice, + // gasLimit, + // nonce + // }, + // value // null or ETH Big number + // } + function prepareData(method, data, hexToDec) { + let payload = null + switch(method) { + case methods.personalSign.name: { + payload = methods.personalSign.getMessageFromData(data) + break + } + case methods.sign.name: { + payload = methods.sign.getMessageFromData(data) + break + } + case methods.signTypedData_v4.name: { + const stringPayload = methods.signTypedData_v4.getMessageFromData(data) + payload = JSON.stringify(JSON.parse(stringPayload), null, 2) + break + } + case methods.signTypedData.name: { + const stringPayload = methods.signTypedData.getMessageFromData(data) + payload = JSON.stringify(JSON.parse(stringPayload), null, 2) + break + } + case methods.signTransaction.name: + case methods.sendTransaction.name: + // For transactions we process the data in a different way as follows + break + default: + console.error("Unhandled method", method) + break; + } + + let value = SQUtils.AmountsArithmetic.fromNumber(0) + let txObj = getTxObject(method, data) + if (isTransactionMethod(method)) { + payload = parseTransaction(txObj, hexToDec) + if (payload.hasOwnProperty("value")) { + value = payload.value + } + } + return { + signData: payload, + transaction: txObj, + value: value + } + } + + /// Parses the transaction object and converts the values to human readable format + function parseTransaction(tx, hexToDec) { + let parsedTransaction = Object.assign({}, tx) + if (parsedTransaction.hasOwnProperty("value")) { + parsedTransaction.value = hexToEth(parsedTransaction.value, hexToDec).toString() + } + if (parsedTransaction.hasOwnProperty("maxFeePerGas")) { + parsedTransaction.maxFeePerGas = hexToGwei(parsedTransaction.maxFeePerGas, hexToDec).toString() + } + if (parsedTransaction.hasOwnProperty("maxPriorityFeePerGas")) { + parsedTransaction.maxPriorityFeePerGas = hexToGwei(parsedTransaction.maxPriorityFeePerGas, hexToDec).toString() + } + if (parsedTransaction.hasOwnProperty("gasPrice")) { + parsedTransaction.gasPrice = hexToGwei(parsedTransaction.gasPrice, hexToDec) + } + if (parsedTransaction.hasOwnProperty("gasLimit")) { + parsedTransaction.gasLimit = parseInt(hexToDec(parsedTransaction.gasLimit)) + } + if (parsedTransaction.hasOwnProperty("nonce")) { + parsedTransaction.nonce = parseInt(hexToDec(parsedTransaction.nonce)) + } + return parsedTransaction + } + + function hexToEth(value, hexToDec) { + return hexToEthDenomination(value, "eth", hexToDec) + } + function hexToGwei(value, hexToDec) { + return hexToEthDenomination(value, "gwei", hexToDec) + } + function hexToEthDenomination(value, ethUnit, hexToDec) { + let unitMapping = { + "gwei": 9, + "eth": 18 + } + let BigOps = SQUtils.AmountsArithmetic + let decValue = hexToDec(value) + if (!!decValue) { + return BigOps.div(BigOps.fromNumber(decValue), BigOps.fromNumber(1, unitMapping[ethUnit])) + } + return BigOps.fromNumber(0) + } + + function getTxObject(method, data) { + let tx + if (method === methods.signTransaction.name) { + tx = methods.signTransaction.getTxObjFromData(data) + } else if (method === methods.sendTransaction.name) { + tx = methods.sendTransaction.getTxObjFromData(data) + } + return tx + } + + function isTransactionMethod(method) { + return method === methods.signTransaction.name + || method === methods.sendTransaction.name + } + + function getExpiryDate(event) { + return event.params.request.expiryTimestamp + } } \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestResolved.qml b/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestResolved.qml index d1a604d1e3..8237cacb00 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestResolved.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestResolved.qml @@ -23,6 +23,7 @@ QObject { required property string chainId // optional expiry date in ms property var expirationTimestamp + property bool active: false // Maps to Constants.DAppConnectors values required property int sourceId @@ -31,32 +32,22 @@ QObject { // Data prepared for display in a human readable format required property var preparedData - property alias dappName: d.dappName - property alias dappUrl: d.dappUrl - property alias dappIcon: d.dappIcon + required property string dappName + required property string dappUrl + required property string dappIcon /// extra data resolved from wallet - property string maxFeesText: "" - property string maxFeesEthText: "" property bool haveEnoughFunds: false property bool haveEnoughFees: false property var /* Big */ fiatMaxFees property var /* Big */ ethMaxFees property var feesInfo + property var /* Big */ value /// maps to Constants.TransactionEstimatedTime values property int estimatedTimeCategory: 0 - function resolveDappInfoFromSession(session) { - let meta = session.peer.metadata - d.dappName = meta.name - d.dappUrl = meta.url - if (meta.icons && meta.icons.length > 0) { - d.dappIcon = meta.icons[0] - } - } - function isExpired() { return !!expirationTimestamp && expirationTimestamp > 0 && Math.floor(Date.now() / 1000) >= expirationTimestamp } @@ -65,13 +56,7 @@ QObject { expirationTimestamp = Math.floor(Date.now() / 1000) } - // dApp info - QtObject { - id: d - - property string dappName - property string dappUrl - property url dappIcon - property bool hasExpiry + function setActive() { + active = true } } \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestResolver.qml b/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestResolver.qml new file mode 100644 index 0000000000..34494a44c3 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestResolver.qml @@ -0,0 +1,78 @@ +pragma Singleton + +import QtQuick 2.15 + +import StatusQ.Core.Utils 0.1 as SQUtils + +/// Component that resolves a session request event +/// and returns a validated SessionRequest js object +/// @returns { +/// error: SessionRequest.Error, +/// request: { +/// event, - original event +/// topic, - dapp session identifier +/// requestId, - unique request identifier +/// method, - RPC method +/// account, - account address to sign the request +/// chainId, - chain id +/// data, - challenge data +/// preparedData, - human readable data +/// expiryTimestamp, - request expiry timestamp +/// transaction: { +/// value, +/// maxFeePerGas, +/// maxPriorityFeePerGas, +/// gasPrice, +/// gasLimit, +/// nonce +/// } +/// } +///} +SQUtils.QObject { + id: root + + function resolveEvent(event, accountsModel, networksModel, hexToDec) { + if (!event) { + console.warn("SessionRequestResolver - resolveEvent - invalid event") + return { request: null, error: SessionRequest.InvalidEvent } + } + if (!accountsModel) { + console.warn("SessionRequestResolver - resolveEvent - invalid accountsModel") + return { request: null, error: SessionRequest.RuntimeError } + } + if (!networksModel) { + console.warn("SessionRequestResolver - resolveEvent - invalid networksModel") + return { request: null, error: SessionRequest.RuntimeError } + } + + try { + const { request, error } = SessionRequest.parse(event, hexToDec) + if (error) { + console.warn("SessionRequestResolver - resolveEvent - failed to build request", error) + return { request: null, error } + } + if (!request) { + console.warn("SessionRequestResolver - resolveEvent - failed to build request") + return { request: null, error: SessionRequest.RuntimeError } + } + const validChainId = !!SQUtils.ModelUtils.getByKey(networksModel, "chainId", request.chainId) + if (!validChainId) { + console.warn("SessionRequestResolver - resolveEvent - invalid chainId", request.chainId) + return { request: null, error: SessionRequest.InvalidChainId } + } + + const validAccount = !!SQUtils.ModelUtils.getFirstModelEntryIf(accountsModel, (account) => { + return account.address.toLowerCase() === request.account.toLowerCase(); + }) + if (!validAccount) { + console.warn("SessionRequestResolver - resolveEvent - invalid account", request.account) + return { request: null, error: SessionRequest.InvalidAccount } + } + + return { request, error: SessionRequest.NoError } + } catch (e) { + console.warn("SessionRequestResolver - resolveEvent - failed to resolve event", e) + return { request: null, error: SessionRequest.RuntimeError } + } + } +} \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/types/qmldir b/ui/app/AppLayouts/Wallet/services/dapps/types/qmldir index d5b4409b43..1529591bf8 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/types/qmldir +++ b/ui/app/AppLayouts/Wallet/services/dapps/types/qmldir @@ -2,5 +2,6 @@ SessionRequestResolved 1.0 SessionRequestResolved.qml SessionRequestWithAuth 1.0 SessionRequestWithAuth.qml SessionRequestsModel 1.0 SessionRequestsModel.qml singleton SessionRequest 1.0 SessionRequest.qml +singleton SessionRequestResolver 1.0 SessionRequestResolver.qml singleton Pairing 1.0 Pairing.qml singleton ErrorCodes 1.0 ErrorCodes.qml \ No newline at end of file diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index f0c31db77c..3dbde6b7a9 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -2168,57 +2168,57 @@ Item { } } - Component { - id: dappsConnectorSDK + Loader { + id: dAppsServiceLoader - DappsConnectorSDK { - active: WalletStores.RootStore.walletSectionInst.walletReady - controller: WalletStores.RootStore.dappsConnectorController - wcService: Global.walletConnectService - walletStore: WalletStores.RootStore - store: SharedStores.DAppsStore { - controller: WalletStores.RootStore.walletConnectController - } - loginType: appMain.rootStore.loginType + // It seems some of the functionality of the dapp connector depends on the DAppsService + active: { + return (featureFlagsStore.dappsEnabled || featureFlagsStore.connectorEnabled) && appMain.visible } - } - Loader { - id: dappsConnectorSDKLoader - active: featureFlagsStore.connectorEnabled - sourceComponent: dappsConnectorSDK - } - - Loader { - id: walletConnectServiceLoader - - // It seems some of the functionality of the dapp connector depends on the WalletConnectService - active: (featureFlagsStore.dappsEnabled || featureFlagsStore.connectorEnabled) && appMain.visible - - sourceComponent: WalletConnectService { - id: walletConnectService - - wcSDK: WalletConnectSDK { - enableSdk: WalletStores.RootStore.walletSectionInst.walletReady - userUID: appMain.rootStore.profileSectionStore.profileStore.pubkey - projectId: WalletStores.RootStore.appSettings.walletConnectProjectID + sourceComponent: DAppsService { + id: dAppsService + Component.onCompleted: { + Global.dAppsService = dAppsService } - store: SharedStores.DAppsStore { - controller: WalletStores.RootStore.walletConnectController - } - bcStore: SharedStores.BrowserConnectStore { - controller: WalletStores.RootStore.dappsConnectorController - } - walletRootStore: WalletStores.RootStore - blockchainNetworksDown: appMain.networkConnectionStore.blockchainNetworksDown + // DAppsModule provides the middleware for the dapps + dappsModule: DAppsModule { + currenciesStore: WalletStores.RootStore.currencyStore + groupedAccountAssetsModel: WalletStores.RootStore.walletAssetsStore.groupedAccountAssetsModel + accountsModel: WalletStores.RootStore.nonWatchAccounts + networksModel: SortFilterProxyModel { + sourceModel: WalletStores.RootStore.filteredFlatModel + proxyRoles: [ + FastExpressionRole { + name: "isOnline" + expression: !appMain.networkConnectionStore.blockchainNetworksDown.map(Number).includes(model.chainId) + expectedRoles: "chainId" + } + ] + } + wcSdk: WalletConnectSDK { + enabled: featureFlagsStore.dappsEnabled && WalletStores.RootStore.walletSectionInst.walletReady + userUID: appMain.rootStore.profileSectionStore.profileStore.pubkey + projectId: WalletStores.RootStore.appSettings.walletConnectProjectID + } + bcSdk: DappsConnectorSDK { + enabled: featureFlagsStore.connectorEnabled && WalletStores.RootStore.walletSectionInst.walletReady + store: SharedStores.BrowserConnectStore { + controller: WalletStores.RootStore.dappsConnectorController + } + networksModel: WalletStores.RootStore.filteredFlatModel + accountsModel: WalletStores.RootStore.nonWatchAccounts + } + store: SharedStores.DAppsStore { + controller: WalletStores.RootStore.walletConnectController + } + } + selectedAddress: WalletStores.RootStore.selectedAddress + accountsModel: WalletStores.RootStore.nonWatchAccounts connectorFeatureEnabled: featureFlagsStore.connectorEnabled walletConnectFeatureEnabled: featureFlagsStore.dappsEnabled - Component.onCompleted: { - Global.walletConnectService = walletConnectService - } - onDisplayToastMessage: (message, type) => { const icon = type === Constants.ephemeralNotificationType.danger ? "warning" : type === Constants.ephemeralNotificationType.success ? "checkmark-circle" : "info" diff --git a/ui/imports/shared/panels/AnimatedText.qml b/ui/imports/shared/panels/AnimatedText.qml index 115620d01b..6f8c443f4b 100644 --- a/ui/imports/shared/panels/AnimatedText.qml +++ b/ui/imports/shared/panels/AnimatedText.qml @@ -7,7 +7,7 @@ SequentialAnimation { property var target: null property color fromColor: Theme.palette.directColor1 - property color toColor: Theme.palette.baseColor5 + property color toColor: Theme.palette.getColor(fromColor, 0.1) property int duration: 500 // in milliseconds loops: 3 diff --git a/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml b/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml index 79b481fd03..3bf946f4bd 100644 --- a/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml +++ b/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml @@ -115,6 +115,7 @@ SignTransactionModalBase { AnimatedText { id: maxFeesAnimation target: maxFees + fromColor: maxFees.customColor } } } @@ -214,6 +215,7 @@ SignTransactionModalBase { AnimatedText { id: fiatFeesAnimation target: fiatFees + fromColor: fiatFees.customColor } } StatusTextWithLoadingState { @@ -236,6 +238,7 @@ SignTransactionModalBase { AnimatedText { id: cryptoFeesAnimation target: cryptoFees + fromColor: cryptoFees.customColor } } } diff --git a/ui/imports/shared/stores/BrowserConnectStore.qml b/ui/imports/shared/stores/BrowserConnectStore.qml index aa2e7c878b..40a89c498a 100644 --- a/ui/imports/shared/stores/BrowserConnectStore.qml +++ b/ui/imports/shared/stores/BrowserConnectStore.qml @@ -18,8 +18,8 @@ SQUtils.QObject { signal approveConnectResponse(string id, bool error) signal rejectConnectResponse(string id, bool error) - signal approveTransactionResponse(string requestId, bool error) - signal rejectTransactionResponse(string requestId, bool error) + signal approveTransactionResponse(string topic, string requestId, bool error) + signal rejectTransactionResponse(string topic, string requestId, bool error) function approveConnection(id, account, chainId) { return controller.approveConnection(id, account, chainId) @@ -29,12 +29,12 @@ SQUtils.QObject { return controller.rejectConnection(id, error) } - function approveTransaction(requestId, signature) { - return controller.approveTransaction(requestId, signature) + function approveTransaction(topic, requestId, signature) { + return controller.approveTransaction(topic, requestId, signature) } - function rejectTransaction(requestId, error) { - return controller.rejectTransaction(requestId, error) + function rejectTransaction(topic, requestId, error) { + return controller.rejectTransaction(topic, requestId, error) } function disconnect(id) { @@ -72,12 +72,12 @@ SQUtils.QObject { root.rejectConnectResponse(id, error) } - function onApproveTransactionResponse(requestId, error) { - root.approveTransactionResponse(requestId, error) + function onApproveTransactionResponse(topic, requestId, error) { + root.approveTransactionResponse(topic, requestId, error) } - function onRejectTransactionResponse(requestId, error) { - root.rejectTransactionResponse(requestId, error) + function onRejectTransactionResponse(topic, requestId, error) { + root.rejectTransactionResponse(topic, requestId, error) } } } \ No newline at end of file diff --git a/ui/imports/utils/Global.qml b/ui/imports/utils/Global.qml index d5111d97b6..1b956c4bd9 100644 --- a/ui/imports/utils/Global.qml +++ b/ui/imports/utils/Global.qml @@ -10,7 +10,7 @@ QtObject { property bool appIsReady: false // use the generic var as type to break the cyclic dependency - property var walletConnectService: null + property var dAppsService: null signal openPinnedMessagesPopupRequested(var store, var messageStore, var pinnedMessagesModel, string messageToPin, string chatId) signal openCommunityProfilePopupRequested(var store, var community, var chatCommunitySectionModule)