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