From 4d080e12aa6885adc8f6f577454abc76aff0c938 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 4 Jun 2024 23:45:03 +0300 Subject: [PATCH] feat(dapps) authenticate user for session request Trigger authentication with identity of the request Track the authentication using the identity and allow only once Add tests for the new functionality Minor improvements around the code Closes #15018 --- src/app/boot/app_controller.nim | 5 - .../modules/main/wallet_section/module.nim | 3 +- .../wallet_connect/controller.nim | 17 +- .../service/wallet_connect/service.nim | 43 +++++- storybook/pages/DAppsWorkflowPage.qml | 50 +++++- .../qmlTests/tests/tst_DAppsWorkflow.qml | 145 +++++++++++++----- .../Wallet/panels/DAppsWorkflow.qml | 24 ++- .../services/dapps/DAppsRequestHandler.qml | 133 ++++++++++++++-- .../services/dapps/WalletConnectSDK.qml | 22 +-- .../services/dapps/WalletConnectSDKBase.qml | 2 +- .../services/dapps/WalletConnectService.qml | 3 + .../dapps/types/SessionRequestsModel.qml | 15 +- ui/app/mainui/AppMain.qml | 5 + .../popups/walletconnect/DAppRequestModal.qml | 24 ++- ui/imports/shared/stores/DAppsStore.qml | 23 +++ 15 files changed, 423 insertions(+), 91 deletions(-) diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index a619eaf371..1ce4289008 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -14,7 +14,6 @@ import app_service/service/collectible/service as collectible_service import app_service/service/currency/service as currency_service import app_service/service/transaction/service as transaction_service import app_service/service/wallet_account/service as wallet_account_service -import app_service/service/wallet_connect/service as wallet_connect_service import app_service/service/bookmarks/service as bookmark_service import app_service/service/dapp_permissions/service as dapp_permissions_service import app_service/service/privacy/service as privacy_service @@ -81,7 +80,6 @@ type currencyService: currency_service.Service transactionService: transaction_service.Service walletAccountService: wallet_account_service.Service - walletConnectService: wallet_connect_service.Service bookmarkService: bookmark_service.Service dappPermissionsService: dapp_permissions_service.Service providerService: provider_service.Service @@ -192,7 +190,6 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = statusFoundation.events, statusFoundation.threadpool, result.settingsService, result.accountsService, result.tokenService, result.networkService, result.currencyService ) - result.walletConnectService = wallet_connect_service.newService(statusFoundation.events, statusFoundation.threadpool, result.settingsService) result.messageService = message_service.newService( statusFoundation.events, statusFoundation.threadpool, @@ -328,7 +325,6 @@ proc delete*(self: AppController) = self.tokenService.delete self.transactionService.delete self.walletAccountService.delete - self.walletConnectService.delete self.aboutService.delete self.networkService.delete self.activityCenterService.delete @@ -456,7 +452,6 @@ proc load(self: AppController) = self.collectibleService.init() self.currencyService.init() self.walletAccountService.init() - self.walletConnectService.init() # Apply runtime log level settings if not main_constants.runtimeLogLevelSet(): diff --git a/src/app/modules/main/wallet_section/module.nim b/src/app/modules/main/wallet_section/module.nim index 6a50dfa05a..4cf67fe38c 100644 --- a/src/app/modules/main/wallet_section/module.nim +++ b/src/app/modules/main/wallet_section/module.nim @@ -168,7 +168,7 @@ proc newModule*( result.filter = initFilter(result.controller) result.walletConnectService = wc_service.newService(result.events, result.threadpool, settingsService) - result.walletConnectController = wc_controller.newController(result.walletConnectService) + result.walletConnectController = wc_controller.newController(result.walletConnectService, walletAccountService) result.view = newView(result, result.activityController, result.tmpActivityControllers, result.activityDetailsController, result.collectibleDetailsController, result.walletConnectController) @@ -341,6 +341,7 @@ method load*(self: Module) = self.overviewModule.load() self.sendModule.load() self.networksModule.load() + self.walletConnectService.init() method isLoaded*(self: Module): bool = return self.moduleLoaded diff --git a/src/app/modules/shared_modules/wallet_connect/controller.nim b/src/app/modules/shared_modules/wallet_connect/controller.nim index 61112805fb..7ae6027e24 100644 --- a/src/app/modules/shared_modules/wallet_connect/controller.nim +++ b/src/app/modules/shared_modules/wallet_connect/controller.nim @@ -2,6 +2,7 @@ import NimQml import chronicles import app_service/service/wallet_connect/service as wallet_connect_service +import app_service/service/wallet_account/service as wallet_account_service logScope: topics = "wallet-connect-controller" @@ -10,14 +11,16 @@ QtObject: type Controller* = ref object of QObject service: wallet_connect_service.Service + walletAccountService: wallet_account_service.Service proc delete*(self: Controller) = self.QObject.delete - proc newController*(service: wallet_connect_service.Service): Controller = + proc newController*(service: wallet_connect_service.Service, walletAccountService: wallet_account_service.Service): Controller = new(result, delete) result.service = service + result.walletAccountService = walletAccountService result.QObject.setup @@ -34,3 +37,15 @@ QtObject: else: self.dappsListReceived(res) return true + + proc userAuthenticationResult*(self: Controller, topic: string, id: string, error: bool) {.signal.} + + # Beware, it will fail if an authentication is already in progress + proc authenticateUser*(self: Controller, topic: string, id: string, address: string): bool {.slot.} = + let acc = self.walletAccountService.getAccountByAddress(address) + if acc.keyUid == "": + return false + + return self.service.authenticateUser(acc.keyUid, proc(success: bool) = + self.userAuthenticationResult(topic, id, success) + ) \ No newline at end of file diff --git a/src/app_service/service/wallet_connect/service.nim b/src/app_service/service/wallet_connect/service.nim index ae15e27e5a..06e74c39b2 100644 --- a/src/app_service/service/wallet_connect/service.nim +++ b/src/app_service/service/wallet_connect/service.nim @@ -10,17 +10,26 @@ import app/core/eventemitter import app/core/signals/types import app/core/tasks/[threadpool] +import app/modules/shared_modules/keycard_popup/io_interface as keycard_shared_module + logScope: topics = "wallet-connect-service" # include async_tasks +const UNIQUE_WALLET_CONNECT_MODULE_IDENTIFIER* = "WalletSection-WCModule" + +type + AuthenticationResponseFn* = proc(success: bool) + QtObject: type Service* = ref object of QObject events: EventEmitter threadpool: ThreadPool settingsService: settings_service.Service + authenticationCallback: AuthenticationResponseFn + proc delete*(self: Service) = self.QObject.delete @@ -31,12 +40,29 @@ QtObject: ): Service = new(result, delete) result.QObject.setup + result.events = events result.threadpool = threadpool result.settingsService = settings_service proc init*(self: Service) = - discard + self.events.on(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args): + let args = SharedKeycarModuleArgs(e) + if args.uniqueIdentifier != UNIQUE_WALLET_CONNECT_MODULE_IDENTIFIER: + return + + if self.authenticationCallback == nil: + error "unexpected user authenticated event; no callback set" + return + defer: + self.authenticationCallback = nil + + if args.password == "" and args.pin == "": + info "fail to authenticate user" + self.authenticationCallback(false) + return + + self.authenticationCallback(true) proc addSession*(self: Service, session_json: string): bool = # TODO #14588: call it async @@ -46,4 +72,17 @@ QtObject: let validAtEpoch = now().toTime().toUnix() let testChains = self.settingsService.areTestNetworksEnabled() # TODO #14588: call it async - return status_go.getDapps(validAtEpoch, testChains) \ No newline at end of file + return status_go.getDapps(validAtEpoch, testChains) + + # Will fail if another authentication is in progress + proc authenticateUser*(self: Service, keyUid: string, callback: AuthenticationResponseFn): bool = + if self.authenticationCallback != nil: + return false + self.authenticationCallback = callback + + let data = SharedKeycarModuleAuthenticationArgs( + uniqueIdentifier: UNIQUE_WALLET_CONNECT_MODULE_IDENTIFIER, + keyUid: keyUid) + + self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data) + return true \ No newline at end of file diff --git a/storybook/pages/DAppsWorkflowPage.qml b/storybook/pages/DAppsWorkflowPage.qml index c0e24b9ed3..a71bb2b7c0 100644 --- a/storybook/pages/DAppsWorkflowPage.qml +++ b/storybook/pages/DAppsWorkflowPage.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts 1.15 import QtQml 2.15 import Qt.labs.settings 1.0 import QtTest 1.15 +import QtQml.Models 2.14 import StatusQ.Core 0.1 import StatusQ.Core.Utils 0.1 @@ -30,7 +31,6 @@ import shared.stores 1.0 Item { id: root - // qml Splitter SplitView { anchors.fill: parent @@ -213,6 +213,42 @@ Item { } } + StatusDialog { + id: authMockDialog + title: "Authenticate user" + visible: false + + property string topic: "" + property string id: "" + + ColumnLayout { + RowLayout { + StatusBaseText { text: "Topic" } + StatusBaseText { text: authMockDialog.topic } + StatusBaseText { text: "ID" } + StatusBaseText { text: authMockDialog.id } + } + } + footer: StatusDialogFooter { + rightButtons: ObjectModel { + StatusButton { + text: qsTr("Reject") + onClicked: { + walletConnectService.store.userAuthenticationFailed(authMockDialog.topic, authMockDialog.id) + authMockDialog.close() + } + } + StatusButton { + text: qsTr("Authenticate") + onClicked: { + walletConnectService.store.userAuthenticated(authMockDialog.topic, authMockDialog.id) + authMockDialog.close() + } + } + } + } + } + WalletConnectService { id: walletConnectService @@ -224,6 +260,9 @@ Item { store: DAppsStore { signal dappsListReceived(string dappsJson) + signal userAuthenticated(string topic, string id) + signal userAuthenticationFailed(string topic, string id) + signal sessionRequestExecuted(var payload, bool success) function addWalletConnectSession(sessionJson) { console.info("Persist Session", sessionJson) @@ -243,6 +282,13 @@ Item { this.dappsListReceived(JSON.stringify(d.persistedDapps)) return true } + + function authenticateUser(topic, id, address) { + authMockDialog.topic = topic + authMockDialog.id = id + authMockDialog.open() + return true + } } walletStore: WalletStore { @@ -253,6 +299,8 @@ Item { property var accounts: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel readonly property ListModel ownAccounts: accounts } + + onDisplayToastMessage: (message, error) => console.info("Storybook - toast message: ", message, !!error ? "; error: " + error: "") } diff --git a/storybook/qmlTests/tests/tst_DAppsWorkflow.qml b/storybook/qmlTests/tests/tst_DAppsWorkflow.qml index 7847872a7a..c90fa93eed 100644 --- a/storybook/qmlTests/tests/tst_DAppsWorkflow.qml +++ b/storybook/qmlTests/tests/tst_DAppsWorkflow.qml @@ -22,8 +22,8 @@ import utils 1.0 Item { id: root - // width: 600 - // height: 400 + width: 600 + height: 400 // Component { // id: sdkComponent @@ -79,6 +79,9 @@ Item { // DAppsStore { // signal dappsListReceived(string dappsJson) + // signal userAuthenticated(string topic, string id) + // signal userAuthenticationFailed(string topic, string id) + // signal sessionRequestExecuted(var payload, bool success) // // By default, return no dapps in store // function getDapps() { @@ -90,6 +93,16 @@ Item { // function addWalletConnectSession(sessionJson) { // addWalletConnectSessionCalls.push({sessionJson}) // } + + // property var authenticateUserCalls: [] + // function authenticateUser(topic, id, address) { + // authenticateUserCalls.push({topic, id, address}) + // } + + // property var signMessageCalls: [] + // function signMessage(message) { + // signMessageCalls.push({message}) + // } // } // } @@ -120,6 +133,52 @@ Item { // } // } + // Component { + // id: dappsRequestHandlerComponent + + // DAppsRequestHandler { + // } + // } + + // TestCase { + // id: requestHandlerTest + // name: "DAppsRequestHandler" + + // property DAppsRequestHandler handler: null + + // SignalSpy { + // id: displayToastMessageSpy + // target: requestHandlerTest.handler + // signalName: "onDisplayToastMessage" + // } + + // function init() { + // let walletStore = createTemporaryObject(walletStoreComponent, root) + // verify(!!walletStore) + // let sdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab" }) + // verify(!!sdk) + // let store = createTemporaryObject(dappsStoreComponent, root) + // verify(!!store) + // handler = createTemporaryObject(dappsRequestHandlerComponent, root, {sdk: sdk, store: store, walletStore: walletStore}) + // verify(!!handler) + // } + + // function cleanup() { + // displayToastMessageSpy.clear() + // } + + // function test_TestAuthentication() { + // let td = mockSessionRequestEvent(this, handler.sdk, handler.walletStore) + // handler.authenticate(td.request) + // compare(handler.store.authenticateUserCalls.length, 1, "expected a call to store.authenticateUser") + + // let store = handler.store + // store.userAuthenticated(td.topic, td.request.id) + // compare(store.signMessageCalls.length, 1, "expected a call to store.signMessage") + // compare(store.signMessageCalls[0].message, td.request.data) + // } + // } + // TestCase { // id: walletConnectServiceTest // name: "WalletConnectService" @@ -263,6 +322,10 @@ Item { // verify(!!request.data, "expected data to be set") // compare(request.data.message, message, "expected message to be set") // } + + // // TODO #14757: add tests with multiple session requests coming in; validate that authentication is serialized and in order + // // function tst_SessionRequestQueueMultiple() { + // // } // } // Component { @@ -394,7 +457,7 @@ Item { // waitForRendering(controlUnderTest) // compare(dappsListReadySpy.count, 0, "expected NO dappsListReady signal to be emitted") - // mouseClick(controlUnderTest, Qt.LeftButton) + // mouseClick(controlUnderTest) // waitForRendering(controlUnderTest) // compare(dappsListReadySpy.count, 1, "expected dappsListReady signal to be emitted") @@ -402,7 +465,7 @@ Item { // verify(!!popup) // verify(popup.opened) - // mouseClick(Overlay.overlay, Qt.LeftButton) + // popup.close() // waitForRendering(controlUnderTest) // verify(!popup.opened) @@ -411,7 +474,7 @@ Item { // function test_OpenPairModal() { // waitForRendering(controlUnderTest) - // mouseClick(controlUnderTest, Qt.LeftButton) + // mouseClick(controlUnderTest) // waitForRendering(controlUnderTest) // let popup = findChild(controlUnderTest, "dappsPopup") @@ -422,7 +485,7 @@ Item { // verify(!!connectButton) // verify(pairWCReadySpy.count === 0, "expected NO pairWCReady signal to be emitted") - // mouseClick(connectButton, Qt.LeftButton) + // mouseClick(connectButton) // waitForRendering(controlUnderTest) // verify(pairWCReadySpy.count === 1, "expected pairWCReady signal to be emitted") @@ -437,42 +500,11 @@ Item { // } // } - // function mockSessionRequestEvent() { - // let service = controlUnderTest.wcService - // let account = service.walletStore.accounts.get(1) - // let network = service.walletStore.flatNetworks.get(1) - // let method = "personal_sign" - // let message = "hello world" - // let params = [Helpers.strToHex(message), account.address] - // let topic = "b536a" - // let requestEvent = JSON.parse(Testing.formatSessionRequest(network.chainId, method, params, topic)) - // let request = createTemporaryObject(sessionRequestComponent, root, { - // event: requestEvent, - // topic, - // id: requestEvent.id, - // method: Constants.personal_sign, - // account, - // network, - // data: message - // }) - // // All calls to SDK are expected as events to be made by the wallet connect SDK - // let sdk = service.wcSDK - - // // Expect to have calls to getActiveSessions from service initialization - // let prevRequests = sdk.getActiveSessionsCallbacks.length - // sdk.sessionRequestEvent(requestEvent) - // // Service will trigger a sessionRequest event following the getActiveSessions call - // let callback = sdk.getActiveSessionsCallbacks[prevRequests].callback - // let session = JSON.parse(Testing.formatApproveSessionResponse([network.chainId, 7], [account.address])) - // callback({"b536a": session}) - - // return {sdk, session, account, network, topic, id: request.id} - // } - // function test_OpenDappRequestModal() { // waitForRendering(controlUnderTest) - // let td = mockSessionRequestEvent() + // let service = controlUnderTest.wcService + // let td = mockSessionRequestEvent(this, service.wcSDK, service.walletStore) // waitForRendering(controlUnderTest) // let popup = findChild(controlUnderTest, "dappsRequestModal") @@ -494,7 +526,8 @@ Item { // function test_RejectDappRequestModal() { // waitForRendering(controlUnderTest) - // let td = mockSessionRequestEvent() + // let service = controlUnderTest.wcService + // let td = mockSessionRequestEvent(this, service.wcSDK, service.walletStore) // waitForRendering(controlUnderTest) // let popup = findChild(controlUnderTest, "dappsRequestModal") @@ -502,11 +535,11 @@ Item { // let rejectButton = findChild(popup, "rejectButton") - // mouseClick(rejectButton, Qt.LeftButton) + // mouseClick(rejectButton) // compare(td.sdk.rejectSessionRequestCalls.length, 1, "expected a call to service.rejectSessionRequest") // let args = td.sdk.rejectSessionRequestCalls[0] // compare(args.topic, td.topic, "expected topic to be set") - // compare(args.id, td.id, "expected id to be set") + // compare(args.id, td.request.id, "expected id to be set") // compare(args.error, false, "expected no error; it was user rejected") // waitForRendering(controlUnderTest) @@ -514,4 +547,32 @@ Item { // verify(!popup.visible) // } // } + + // function mockSessionRequestEvent(tc, sdk, walletStore) { + // let account = walletStore.accounts.get(1) + // let network = walletStore.flatNetworks.get(1) + // let method = "personal_sign" + // let message = "hello world" + // let params = [Helpers.strToHex(message), account.address] + // let topic = "b536a" + // let requestEvent = JSON.parse(Testing.formatSessionRequest(network.chainId, method, params, topic)) + // let request = tc.createTemporaryObject(sessionRequestComponent, root, { + // event: requestEvent, + // topic, + // id: requestEvent.id, + // method: Constants.personal_sign, + // account, + // network, + // data: message + // }) + // // Expect to have calls to getActiveSessions from service initialization + // let prevRequests = sdk.getActiveSessionsCallbacks.length + // sdk.sessionRequestEvent(requestEvent) + // // Service might trigger a sessionRequest event following the getActiveSessions call + // let callback = sdk.getActiveSessionsCallbacks[prevRequests].callback + // let session = JSON.parse(Testing.formatApproveSessionResponse([network.chainId, 7], [account.address])) + // callback({"b536a": session}) + + // return {sdk, session, account, network, topic, request} + // } } diff --git a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml index d8f222247b..b9519783a2 100644 --- a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml +++ b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml @@ -61,12 +61,13 @@ ConnectedDappsButton { sourceComponent: DAppsListPopup { visible: true - model: wcService.dappsModel + model: root.wcService.dappsModel onPairWCDapp: { pairWCLoader.active = true this.close() } + onOpened: { this.x = root.width - this.contentWidth - 2 * this.padding this.y = root.height + 4 @@ -91,8 +92,8 @@ ConnectedDappsButton { visible: true onClosed: connectDappLoader.active = false - accounts: wcService.validAccounts - flatNetworks: wcService.flatNetworks + accounts: root.wcService.validAccounts + flatNetworks: root.wcService.flatNetworks onConnect: { root.wcService.approvePairSession(sessionProposal, dappChains, selectedAccount) @@ -136,17 +137,30 @@ ConnectedDappsButton { onClosed: sessionRequestLoader.active = false onSign: { - console.debug("@dd TODO sign session request") + if (!request) { + console.error("Error signing: request is null") + return + } + root.wcService.requestHandler.authenticate(request) } onReject: { let userRejected = true - wcService.requestHandler.rejectSessionRequest(request, userRejected) + root.wcService.requestHandler.rejectSessionRequest(request, userRejected) close() } } } + Connections { + target: root.wcService ? root.wcService.requestHandler : null + + function onSessionRequestResult(payload, isSuccess) { + // TODO #14927 handle this properly + sessionRequestLoader.active = false + } + } + Connections { target: root.wcService diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml b/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml index 86f587d88e..70081192f7 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml @@ -23,16 +23,32 @@ QObject { sdk.rejectSessionRequest(request.topic, request.id, error) } + /// Beware, it will fail if called multiple times before getting an answer + function authenticate(request) { + return store.authenticateUser(request.topic, request.id, request.account.address) + } + signal sessionRequest(SessionRequestResolved request) + signal displayToastMessage(string message, bool error) + signal sessionRequestResult(var payload, bool isSuccess) /// Supported methods property QtObject methods: QtObject { - readonly property string personalSign: Constants.personal_sign - readonly property string sendTransaction: "eth_sendTransaction" + readonly property QtObject personalSign: QtObject { + readonly property string name: Constants.personal_sign + readonly property string userString: qsTr("sign") + } + readonly property QtObject sendTransaction: QtObject { + readonly property string name: "eth_sendTransaction" + readonly property string userString: qsTr("send transaction") + } + readonly property var all: [personalSign, sendTransaction] } function getSupportedMethods() { - return [root.methods.personalSign, root.methods.sendTransaction] + return methods.all.map(function(method) { + return method.name + }) } Connections { @@ -47,6 +63,65 @@ QObject { } requests.enqueue(obj) } + + function onSessionRequestUserAnswerResult(topic, id, accept, error) { + var request = requests.findRequest(topic, id) + if (request === null) { + console.error("Error finding event for topic", topic, "id", id) + return + } + let methodStr = d.methodToUserString(request.method) + if (!methodStr) { + console.error("Error finding user string for method", request.method) + return + } + + d.lookupSession(topic, function(session) { + if (session === null) + return + if (error) { + root.displayToastMessage(qsTr("Fail to %1 from %2").arg(methodStr).arg(session.peer.metadata.url), true) + // TODO #14757 handle SDK error on user accept/reject + console.error(`Error accepting session request for topic: ${topic}, id: ${id}, accept: ${accept}, error: ${error}`) + return + } + + let actionStr = accept ? qsTr("accepted") : qsTr("rejected") + root.displayToastMessage("%1 %2 %3".arg(session.peer.metadata.url).arg(methodStr).arg(actionStr), false) + root.sessionRequestApprovalResult() + }) + } + } + + Connections { + target: root.store + + function onUserAuthenticated(topic, id) { + var request = requests.findRequest(topic, id) + if (request === null) { + console.error("Error finding event for topic", topic, "id", id) + return + } + d.executeSessionRequest(request) + } + + function onUserAuthenticationFailed(topic, id) { + var request = requests.findRequest(topic, id) + let methodStr = d.methodToUserString(request.method) + if (request === null || !methodStr) { + return + } + d.lookupSession(topic, function(session) { + if (session === null) + return + root.displayToastMessage(qsTr("Failed to authenticate %1 from %2").arg(methodStr).arg(session.peer.metadata.url), true) + }) + } + + function onSessionRequestExecuted(payload, isSuccess) { + // TODO #14927 handle this properly + root.sessionRequestResult(payload, isSuccess) + } } QObject { @@ -74,19 +149,18 @@ QObject { // Check later to have a valid request object if (!getSupportedMethods().includes(method) // TODO #14927: support method eth_sendTransaction - || method == "eth_sendTransaction") { + || method == root.methods.sendTransaction.name) { console.error("Unsupported method", method) return null } - sdk.getActiveSessions((res) => { - Object.keys(res).forEach((topic) => { - if (topic === obj.topic) { - let session = res[topic] - obj.resolveDappInfoFromSession(session) - root.sessionRequest(obj) - } - }) + d.lookupSession(obj.topic, function(session) { + if (session === null) { + console.error("DAppsRequestHandler.lookupSession: error finding session for topic", obj.topic) + return + } + obj.resolveDappInfoFromSession(session) + root.sessionRequest(obj) }) return obj @@ -94,7 +168,7 @@ QObject { /// Returns null if the account is not found function lookupAccountFromEvent(event, method) { - if (method === root.methods.personalSign) { + if (method === root.methods.personalSign.name) { if (event.params.request.params.length < 2) { return null } @@ -111,7 +185,7 @@ QObject { /// Returns null if the network is not found function lookupNetworkFromEvent(event, method) { - if (method === root.methods.personalSign) { + if (method === root.methods.personalSign.name) { let chainId = Helpers.chainIdFromEip155(event.params.chainId) for (let i = 0; i < walletStore.flatNetworks.count; i++) { let network = ModelUtils.get(walletStore.flatNetworks, i) @@ -124,7 +198,7 @@ QObject { } function extractMethodData(event, method) { - if (method === root.methods.personalSign) { + if (method === root.methods.personalSign.name) { if (event.params.request.params.length == 0) { return null } @@ -134,6 +208,35 @@ QObject { } } } + + function methodToUserString(method) { + for (let i = 0; i < methods.all.length; i++) { + if (methods.all[i].name === method) { + return methods.all[i].userString + } + } + return "" + } + + function lookupSession(topicToLookup, callback) { + sdk.getActiveSessions((res) => { + Object.keys(res).forEach((topic) => { + if (topic === topicToLookup) { + let session = res[topic] + callback(session) + } + }) + }) + } + + function executeSessionRequest(request) { + if (request.method === root.methods.personalSign.name) { + store.signMessage(request.data.message) + console.debug("TODO #14927 sign message: ", request.data.message) + } else { + console.error("Unsupported method to execute: ", request.method) + } + } } /// The queue is used to ensure that the events are processed in the order they are received but they could be diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml index c1d0b5f97d..e2b35b767b 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml @@ -206,10 +206,10 @@ WalletConnectSDKBase { d.engine.runJavaScript(` wc.respondSessionRequest("${topic}", ${id}, "${signature}") .then((value) => { - wc.statusObject.onRespondSessionRequestResponse("") + wc.statusObject.onAcceptSessionRequestResponse("${topic}", ${id}, "") }) .catch((e) => { - wc.statusObject.onRespondSessionRequestResponse(e.message) + wc.statusObject.onAcceptSessionRequestResponse("${topic}", ${id}, e.message) }) ` ) @@ -221,10 +221,10 @@ WalletConnectSDKBase { d.engine.runJavaScript(` wc.rejectSessionRequest("${topic}", ${id}, "${error}") .then((value) => { - wc.statusObject.onRejectSessionRequestResponse("") + wc.statusObject.onRejectSessionRequestResponse("${topic}", ${id}, "") }) .catch((e) => { - wc.statusObject.onRejectSessionRequestResponse(e.message) + wc.statusObject.onRejectSessionRequestResponse("${topic}", ${id}, e.message) }) ` ) @@ -387,14 +387,16 @@ WalletConnectSDKBase { root.rejectSessionResult(error) } - function onRespondSessionRequestResponse(error) { - console.debug(`WC WalletConnectSDK.onRespondSessionRequestResponse; error: ${error}`) - root.sessionRequestUserAnswerResult(true, error) + function onAcceptSessionRequestResponse(topic, id, error) { + console.debug(`WC WalletConnectSDK.onAcceptSessionRequestResponse; topic: ${topic}, id: ${id} error: ${error}`) + let responseToAccept = true + root.sessionRequestUserAnswerResult(topic, id, responseToAccept, error) } - function onRejectSessionRequestResponse(error) { - console.debug(`WC WalletConnectSDK.onRejectSessionRequestResponse; error: ${error}`) - root.sessionRequestUserAnswerResult(false, error) + function onRejectSessionRequestResponse(topic, id, error) { + console.debug(`WC WalletConnectSDK.onRejectSessionRequestResponse; topic: ${topic}, id: ${id}, error: ${error}`) + let responseToAccept = false + root.sessionRequestUserAnswerResult(topic, id, responseToAccept, error) } function onSessionProposal(details) { diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDKBase.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDKBase.qml index 22270df913..073ddc1678 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDKBase.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDKBase.qml @@ -13,7 +13,7 @@ Item { signal approveSessionResult(var approvedNamespaces, string error) signal rejectSessionResult(string error) signal sessionRequestEvent(var sessionRequest) - signal sessionRequestUserAnswerResult(bool accept, string error) + signal sessionRequestUserAnswerResult(string topic, string id, bool accept /* not reject */, string error) signal authRequest(var request) signal authMessageFormated(string formatedMessage, string address) diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml index 6c03bfd29d..9c1a6c5e3f 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml @@ -164,6 +164,9 @@ QObject { onSessionRequest: (request) => { root.sessionRequest(request) } + onDisplayToastMessage: (message, error) => { + root.displayToastMessage(message, error) + } } DAppsListProvider { diff --git a/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestsModel.qml b/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestsModel.qml index 32090e7746..05130d38de 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestsModel.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/types/SessionRequestsModel.qml @@ -4,8 +4,8 @@ import QtQuick 2.15 ListModel { id: root - function enqueue(event) { - root.append(event); + function enqueue(request) { + root.append(request); } function dequeue() { @@ -16,4 +16,15 @@ ListModel { } return null; } + + /// returns null if not found + function findRequest(topic, id) { + for (var i = 0; i < root.count; i++) { + let entry = root.get(i) + if (entry.topic === topic && entry.id === id) { + return entry; + } + } + return null; + } } diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 6a4510d0c4..3693745a27 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -2052,6 +2052,11 @@ Item { Component.onCompleted: { Global.walletConnectService = walletConnectService } + + onDisplayToastMessage: (message, isErr) => { + Global.displayToastMessage(message, "", isErr ? "warning" : "checkmark-circle", false, + isErr ? Constants.ephemeralNotificationType.danger : Constants.ephemeralNotificationType.success, "") + } } } } diff --git a/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml b/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml index 5fd107b4e8..2ddced715e 100644 --- a/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml +++ b/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml @@ -196,11 +196,14 @@ StatusDialog { id: footer leftButtons: ObjectModel { - MaxFeesDisplay {} + MaxFeesDisplay { + } Item { width: 20 } - EstimatedTimeDisplay {} + EstimatedTimeDisplay { + visible: !!root.estimatedTimeText + } } rightButtons: ObjectModel { @@ -218,8 +221,6 @@ StatusDialog { height: 44 text: qsTr("Sign") - enabled: false - onClicked: { root.sign() } @@ -228,21 +229,32 @@ StatusDialog { } component MaxFeesDisplay: ColumnLayout { - visible: root.maxFeesText StatusBaseText { text: qsTr("Max fees:") + font.pixelSize: 12 color: Theme.palette.directColor1 } StatusBaseText { + id: maxFeesDisplay text: root.maxFeesText + + visible: !!root.maxFeesText + font.pixelSize: 16 font.weight: Font.DemiBold } + StatusBaseText { + text: qsTr("No fees") + + visible: !maxFeesDisplay.visible + + font.pixelSize: maxFeesDisplay.font.pixelSize + font.weight: maxFeesDisplay.font.weight + } } component EstimatedTimeDisplay: ColumnLayout { - visible: root.estimatedTimeText StatusBaseText { text: qsTr("Est. time:") font.pixelSize: 12 diff --git a/ui/imports/shared/stores/DAppsStore.qml b/ui/imports/shared/stores/DAppsStore.qml index 39a1bdfe19..83b7d18e5a 100644 --- a/ui/imports/shared/stores/DAppsStore.qml +++ b/ui/imports/shared/stores/DAppsStore.qml @@ -9,11 +9,26 @@ QObject { /// \c dappsJson serialized from status-go.wallet.GetDapps signal dappsListReceived(string dappsJson) + signal userAuthenticated(string topic, string id) + signal userAuthenticationFailed(string topic, string id) + signal sessionRequestExecuted(var payload, bool success) function addWalletConnectSession(sessionJson) { controller.addWalletConnectSession(sessionJson) } + function authenticateUser(topic, id, address) { + let ok = controller.authenticateUser(topic, id, address) + if(!ok) { + root.userAuthenticationFailed() + } + } + + function signMessage(message) { + // TODO #14927 implement me + root.sessionRequestExecuted(message, true) + } + /// \c getDapps triggers an async response to \c dappsListReceived function getDapps() { return controller.getDapps() @@ -26,5 +41,13 @@ QObject { function onDappsListReceived(dappsJson) { root.dappsListReceived(dappsJson) } + + function onUserAuthenticationResult(topic, id, success) { + if (success) { + root.userAuthenticated(topic, id) + } else { + root.userAuthenticationFailed(topic, id) + } + } } } \ No newline at end of file