diff --git a/src/app/modules/shared_modules/wallet_connect/controller.nim b/src/app/modules/shared_modules/wallet_connect/controller.nim index 7ae6027e24..828478f823 100644 --- a/src/app/modules/shared_modules/wallet_connect/controller.nim +++ b/src/app/modules/shared_modules/wallet_connect/controller.nim @@ -38,7 +38,7 @@ QtObject: self.dappsListReceived(res) return true - proc userAuthenticationResult*(self: Controller, topic: string, id: string, error: bool) {.signal.} + proc userAuthenticationResult*(self: Controller, topic: string, id: string, error: bool, password: string, pin: string) {.signal.} # Beware, it will fail if an authentication is already in progress proc authenticateUser*(self: Controller, topic: string, id: string, address: string): bool {.slot.} = @@ -46,6 +46,12 @@ QtObject: 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 + return self.service.authenticateUser(acc.keyUid, proc(password: string, pin: string, success: bool) = + self.userAuthenticationResult(topic, id, success, password, pin) + ) + + proc signMessage*(self: Controller, address: string, password: string, message: string): string {.slot.} = + return self.service.signMessage(address, password, message) + + proc signTypedDataV4*(self: Controller, address: string, password: string, typedDataJson: string): string {.slot.} = + return self.service.signTypedDataV4(address, password, typedDataJson) diff --git a/src/app_service/service/wallet_connect/service.nim b/src/app_service/service/wallet_connect/service.nim index 06e74c39b2..2f460a290e 100644 --- a/src/app_service/service/wallet_connect/service.nim +++ b/src/app_service/service/wallet_connect/service.nim @@ -20,7 +20,7 @@ logScope: const UNIQUE_WALLET_CONNECT_MODULE_IDENTIFIER* = "WalletSection-WCModule" type - AuthenticationResponseFn* = proc(success: bool) + AuthenticationResponseFn* = proc(password: string, pin: string, success: bool) QtObject: type Service* = ref object of QObject @@ -59,10 +59,10 @@ QtObject: if args.password == "" and args.pin == "": info "fail to authenticate user" - self.authenticationCallback(false) + self.authenticationCallback("", "", false) return - self.authenticationCallback(true) + self.authenticationCallback(args.password, args.pin, true) proc addSession*(self: Service, session_json: string): bool = # TODO #14588: call it async @@ -85,4 +85,10 @@ QtObject: keyUid: keyUid) self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data) - return true \ No newline at end of file + return true + + proc signMessage*(self: Service, address: string, password: string, message: string): string = + return status_go.signMessage(address, password, message) + + proc signTypedDataV4*(self: Service, address: string, password: string, typedDataJson: string): string = + return status_go.signTypedData(address, password, typedDataJson) diff --git a/src/backend/wallet_connect.nim b/src/backend/wallet_connect.nim index 8a60094cd1..0b4f4f886c 100644 --- a/src/backend/wallet_connect.nim +++ b/src/backend/wallet_connect.nim @@ -1,20 +1,32 @@ import options, logging import json, json_serialization import core, response_type +import strutils from gen import rpc import backend +import status_go + +import app_service/service/community/dto/sign_params + +import app_service/common/utils + rpc(addWalletConnectSession, "wallet"): sessionJson: string -proc isErrorResponse(rpcResponse: RpcResponse[JsonNode]): bool = - return not rpcResponse.error.isNil +rpc(signTypedDataV4, "wallet"): + typedJson: string + address: string + password: string + +proc isSuccessResponse(rpcResponse: RpcResponse[JsonNode]): bool = + return rpcResponse.error.isNil proc addSession*(sessionJson: string): bool = try: let rpcRes = addWalletConnectSession(sessionJson) - return isErrorResponse(rpcRes): + return isSuccessResponse(rpcRes): except Exception as e: warn "AddWalletConnectSession failed: ", "msg", e.msg return false @@ -33,3 +45,29 @@ proc getDapps*(validAtEpoch: int64, testChains: bool): string = except Exception as e: warn "GetWalletConnectDapps failed: ", "msg", e.msg return "" + +proc signMessage*(address: string, password: string, message: string): string = + try: + let signParams = SignParamsDto(address: address, password: hashPassword(password), data: "0x" & toHex(message)) + let paramsStr = $toJson(signParams) + let rpcResRaw = status_go.signMessage(paramsStr) + + let rpcRes = Json.decode(rpcResRaw, RpcResponse[JsonNode]) + if(not rpcRes.error.isNil): + return "" + return rpcRes.result.getStr() + except Exception as e: + warn "status_go.signMessage failed: ", "msg", e.msg + return "" + +proc signTypedData*(address: string, password: string, typedDataJson: string): string = + try: + let rpcRes = signTypedDataV4(typedDataJson, address, hashPassword(password)) + + if not isSuccessResponse(rpcRes): + return "" + + return rpcRes.result.getStr() + except Exception as e: + warn "wallet_signTypedDataV4 failed: ", "msg", e.msg + return "" diff --git a/storybook/pages/DAppRequestModalPage.qml b/storybook/pages/DAppRequestModalPage.qml index 805b4aebb6..69452c7ce7 100644 --- a/storybook/pages/DAppRequestModalPage.qml +++ b/storybook/pages/DAppRequestModalPage.qml @@ -50,7 +50,8 @@ Item { dappName: settings.dappName dappUrl: settings.dappUrl dappIcon: settings.dappIcon - signContent: JSON.stringify(d.signTestContent, null, 2) + signContent: d.signTestContent + method: "eth_signTypedData_v4" maxFeesText: "1.82 EUR" estimatedTimeText: "3-5 mins" @@ -128,36 +129,7 @@ Item { readonly property var selectedNetwork: NetworksModel.flatNetworks.get(0) - readonly property var signTestContent: { - "id": 1714038548266495, - "params": { - "chainld": "eip155:11155111", - "request": { - "expiryTimestamp": 1714038848, - "method": "eth_signTransaction", - "params": [ - { - "data": "0x", - "from": "0xE2d622C817878dA5143bBE06866ca8E35273Ba8", - "gasLimit": "0x5208", - "gasPrice": "0xa677ef31", - "nonce": "0x27", - "to": "0xE2d622C817878dA5143bBE06866ca8E35273Ba8a", - "value": "0x00" - } - ] - } - }, - "topic": "a0f85b23a1f3a540d85760a523963165fb92169d57320c", - "verifyContext": { - "verified": { - "isScam": false, - "origin": "https://react-app.walletconnect.com/", - "validation": "VALID", - "verifyUrl": "https://verify.walletconnect.com/" - } - } - } + readonly property var signTestContent: "{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallet\",\"type\":\"address\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person\"},{\"name\":\"contents\",\"type\":\"string\"}]},\"primaryType\":\"Mail\",\"domain\":{\"name\":\"Ether Mail\",\"version\":\"1\",\"chainId\":1,\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\"},\"message\":{\"from\":{\"name\":\"Cow\",\"wallet\":\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\"},\"to\":{\"name\":\"Bob\",\"wallet\":\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\"},\"contents\":\"Hello, Bob!\"}}" } } diff --git a/storybook/pages/DAppsWorkflowPage.qml b/storybook/pages/DAppsWorkflowPage.qml index a71bb2b7c0..f6d4b757cd 100644 --- a/storybook/pages/DAppsWorkflowPage.qml +++ b/storybook/pages/DAppsWorkflowPage.qml @@ -241,7 +241,7 @@ Item { StatusButton { text: qsTr("Authenticate") onClicked: { - walletConnectService.store.userAuthenticated(authMockDialog.topic, authMockDialog.id) + walletConnectService.store.userAuthenticated(authMockDialog.topic, authMockDialog.id, "0x1234567890", "123") authMockDialog.close() } } @@ -260,9 +260,8 @@ Item { store: DAppsStore { signal dappsListReceived(string dappsJson) - signal userAuthenticated(string topic, string id) + signal userAuthenticated(string topic, string id, string password, string pin) signal userAuthenticationFailed(string topic, string id) - signal sessionRequestExecuted(var payload, bool success) function addWalletConnectSession(sessionJson) { console.info("Persist Session", sessionJson) @@ -276,6 +275,7 @@ Item { "iconUrl": firstIconUrl } d.persistedDapps.push(persistedDapp) + return true } function getDapps() { @@ -289,6 +289,16 @@ Item { authMockDialog.open() return true } + + // hardcoded for https://react-app.walletconnect.com/ + function signMessage(topic, id, address, password, message) { + return "0x0b083acc1b3b612dd38e8e725b28ce9b2dd4936b4cf7922da4e4a3c6f44f7f4f6d3050ccb41455a2b85093f1bfadb10fc6a75d83bb590b2eb70e3447653459701c" + } + + // hardcoded for https://react-app.walletconnect.com/ + function signTypedDataV4(topic, id, address, password, typedDataJson) { + return "0xf8ceb3468319cc215523b67c24c4504b3addd9bf8de31c278038d7478c9b6de554f7d8a516cd5d6a066b7d48b81f03d9d6bb7d5d754513c08325674ebcc7efbc1b" + } } walletStore: WalletStore { diff --git a/storybook/qmlTests/tests/tst_DAppsWorkflow.qml b/storybook/qmlTests/tests/tst_DAppsWorkflow.qml index c90fa93eed..f86082f202 100644 --- a/storybook/qmlTests/tests/tst_DAppsWorkflow.qml +++ b/storybook/qmlTests/tests/tst_DAppsWorkflow.qml @@ -25,6 +25,7 @@ Item { width: 600 height: 400 + // TODO #15151 fix CI crash and re-enable tests // Component { // id: sdkComponent @@ -79,9 +80,8 @@ Item { // DAppsStore { // signal dappsListReceived(string dappsJson) - // signal userAuthenticated(string topic, string id) + // signal userAuthenticated(string topic, string id, string password, string pin) // signal userAuthenticationFailed(string topic, string id) - // signal sessionRequestExecuted(var payload, bool success) // // By default, return no dapps in store // function getDapps() { @@ -100,8 +100,12 @@ Item { // } // property var signMessageCalls: [] - // function signMessage(message) { - // signMessageCalls.push({message}) + // function signMessage(topic, id, address, password, message) { + // signMessageCalls.push({topic, id, address, password, message}) + // } + // property var signTypedDataV4Calls: [] + // function signTypedDataV4(topic, id, address, password, message) { + // signTypedDataV4Calls.push({topic, id, address, password, message}) // } // } // } @@ -173,7 +177,7 @@ Item { // compare(handler.store.authenticateUserCalls.length, 1, "expected a call to store.authenticateUser") // let store = handler.store - // store.userAuthenticated(td.topic, td.request.id) + // store.userAuthenticated(td.topic, td.request.id, "password", "") // compare(store.signMessageCalls.length, 1, "expected a call to store.signMessage") // compare(store.signMessageCalls[0].message, td.request.data) // } @@ -414,7 +418,7 @@ Item { // } // } - // // Beware this TestCase should be last; I had it before ServiceHelpers and it was not run with `when: windowShown` + // TODO #15151: this TestCase if placed before ServiceHelpers was not run with `when: windowShown`. Check if related to the CI crash // TestCase { // id: dappsWorkflowTest diff --git a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml index b9519783a2..6dc668d208 100644 --- a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml +++ b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml @@ -129,6 +129,7 @@ ConnectedDappsButton { dappIcon: request.dappIcon signContent: request.data.message + method: request.method maxFeesText: request.maxFeesText estimatedTimeText: request.estimatedTimeText @@ -155,9 +156,12 @@ ConnectedDappsButton { Connections { target: root.wcService ? root.wcService.requestHandler : null - function onSessionRequestResult(payload, isSuccess) { - // TODO #14927 handle this properly - sessionRequestLoader.active = false + function onSessionRequestResult(request, payload, isSuccess) { + if (isSuccess) { + sessionRequestLoader.active = false + } else { + // TODO #14762 handle the error case + } } } diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml b/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml index 70081192f7..fe98c970bd 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml @@ -30,7 +30,7 @@ QObject { signal sessionRequest(SessionRequestResolved request) signal displayToastMessage(string message, bool error) - signal sessionRequestResult(var payload, bool isSuccess) + signal sessionRequestResult(/*model entry of SessionRequestResolved*/ var request, var payload, bool isSuccess) /// Supported methods property QtObject methods: QtObject { @@ -38,11 +38,16 @@ QObject { readonly property string name: Constants.personal_sign readonly property string userString: qsTr("sign") } + readonly property QtObject signTypedData_v4: QtObject { + readonly property string name: "eth_signTypedData_v4" + readonly property string userString: qsTr("sign typed data") + } + readonly property QtObject sendTransaction: QtObject { readonly property string name: "eth_sendTransaction" readonly property string userString: qsTr("send transaction") } - readonly property var all: [personalSign, sendTransaction] + readonly property var all: [personalSign, signTypedData_v4, sendTransaction] } function getSupportedMethods() { @@ -81,14 +86,17 @@ QObject { 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 + + root.sessionRequestResult(request, "", false /*isSuccessful*/) + 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() + + root.sessionRequestResult(request, "", true /*isSuccessful*/) }) } } @@ -96,13 +104,13 @@ QObject { Connections { target: root.store - function onUserAuthenticated(topic, id) { + 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) + d.executeSessionRequest(request, password, pin) } function onUserAuthenticationFailed(topic, id) { @@ -117,11 +125,6 @@ QObject { 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 { @@ -168,24 +171,24 @@ QObject { /// Returns null if the account is not found function lookupAccountFromEvent(event, method) { + var address = "" if (method === root.methods.personalSign.name) { if (event.params.request.params.length < 2) { return null } - var address = event.params.request.params[1] - for (let i = 0; i < walletStore.ownAccounts.count; i++) { - let acc = ModelUtils.get(walletStore.ownAccounts, i) - if (acc.address === address) { - return acc - } + address = event.params.request.params[1] + } else if(method === root.methods.signTypedData_v4.name) { + if (event.params.request.params.length < 2) { + return null } + address = event.params.request.params[0] } - return null + return ModelUtils.getByKey(walletStore.ownAccounts, "address", address) } /// Returns null if the network is not found function lookupNetworkFromEvent(event, method) { - if (method === root.methods.personalSign.name) { + if (method === root.methods.personalSign.name || method === root.methods.signTypedData_v4.name) { let chainId = Helpers.chainIdFromEip155(event.params.chainId) for (let i = 0; i < walletStore.flatNetworks.count; i++) { let network = ModelUtils.get(walletStore.flatNetworks, i) @@ -202,9 +205,22 @@ QObject { if (event.params.request.params.length == 0) { return null } - let hexMessage = event.params.request.params[0] + var message = "" + let messageParam = event.params.request.params[0] + // There is no standard on how data is encoded. Therefore we support hex or utf8 + if (Helpers.isHex(messageParam)) { + message = Helpers.hexToString(messageParam) + } else { + message = messageParam + } + return {message} + } else if (method === root.methods.signTypedData_v4.name) { + if (event.params.request.params.length < 2) { + return null + } + let jsonMessage = event.params.request.params[1] return { - message: Helpers.hexToString(hexMessage) + message: jsonMessage } } } @@ -229,10 +245,33 @@ QObject { }) } - function executeSessionRequest(request) { - if (request.method === root.methods.personalSign.name) { - store.signMessage(request.data.message) - console.debug("TODO #14927 sign message: ", request.data.message) + function executeSessionRequest(request, password, pin) { + if (request.method === root.methods.personalSign.name || request.method === root.methods.signTypedData_v4.name) { + if (password !== "") { + //let originalMessage = request.data.message + // TODO #14756: clarify why prefixing the message fails the test app https://react-app.walletconnect.com/ + //let finalMessage = "\x19Ethereum Signed Message:\n" + originalMessage.length + originalMessage + let finalMessage = request.data.message + var signedMessage = "" + if (request.method === root.methods.personalSign.name) { + signedMessage = store.signMessage(request.topic, request.id, + request.account.address, password, finalMessage) + } else if (request.method === root.methods.signTypedData_v4.name) { + signedMessage = store.signTypedDataV4(request.topic, request.id, + request.account.address, password, finalMessage) + } + let isSuccessful = signedMessage != "" + if (isSuccessful) { + // acceptSessionRequest will trigger an sdk.sessionRequestUserAnswerResult signal + sdk.acceptSessionRequest(request.topic, request.id, signedMessage) + } else { + root.sessionRequestResult(request, request.data.message, isSuccessful) + } + } else if (pin !== "") { + console.debug("TODO #14927 sign message using keycard: ", request.data.message) + } else { + console.error("No password or pin provided to sign message") + } } else { console.error("Unsupported method to execute: ", request.method) } diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml index e2b35b767b..bf824d88de 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml @@ -127,7 +127,7 @@ WalletConnectSDKBase { if (d.engine) { d.engine.runJavaScript(`wc.getActiveSessions()`, function(result) { - let allSessions = "" + var allSessions = "" for (var key of Object.keys(result)) { allSessions += `\nsessionTopic: ${key} relatedPairingTopic: ${result[key].pairingTopic}`; } diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml index 9c1a6c5e3f..a427ef38bd 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml @@ -99,7 +99,9 @@ QObject { root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_url), false) // Persist session - store.addWalletConnectSession(JSON.stringify(session)) + if(!store.addWalletConnectSession(JSON.stringify(session))) { + console.error("Failed to persist session") + } // Notify client root.approveSessionResult(session, err) diff --git a/ui/app/AppLayouts/Wallet/services/dapps/helpers.js b/ui/app/AppLayouts/Wallet/services/dapps/helpers.js index 01c449ced5..312dd46d92 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/helpers.js +++ b/ui/app/AppLayouts/Wallet/services/dapps/helpers.js @@ -4,6 +4,10 @@ function chainIdFromEip155(chain) { return parseInt(chain.split(':').pop().trim(), 10) } +function isHex(str) { + return str.startsWith('0x') && str.length % 2 === 0 && /^[0-9a-fA-F]*$/.test(str.slice(2)) +} + function hexToString(hex) { if (hex.startsWith("0x")) { hex = hex.substring(2); diff --git a/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml b/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml index 2ddced715e..19031829df 100644 --- a/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml +++ b/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml @@ -21,6 +21,7 @@ StatusDialog { required property string dappName required property string dappUrl required property url dappIcon + required property string method required property string signContent required property string maxFeesText required property string estimatedTimeText @@ -35,6 +36,10 @@ StatusDialog { padding: 20 + onSignContentChanged: d.updatePayloadToDisplay() + onMethodChanged: d.updatePayloadToDisplay() + Component.onCompleted: d.updatePayloadToDisplay() + contentItem: StatusScrollView { id: scrollView padding: 0 @@ -50,7 +55,6 @@ StatusDialog { dappName: root.dappName dappIcon: root.dappIcon account: root.account - signContent: root.signContent } ContentPanel { @@ -273,7 +277,6 @@ StatusDialog { required property string dappName required property url dappIcon required property var account - required property string signContent // Icons Item { @@ -405,10 +408,24 @@ StatusDialog { width: contentScrollView.availableWidth - text: signContent + text: d.payloadToDisplay wrapMode: Text.WrapAnywhere } } } + + QtObject { + id: d + + property string payloadToDisplay: "" + + function updatePayloadToDisplay() { + if (root.method === "eth_signTypedData_v4" && root.signContent) { + payloadToDisplay = JSON.stringify(JSON.parse(root.signContent), null, 2) + return + } + payloadToDisplay = root.signContent + } + } } diff --git a/ui/imports/shared/stores/DAppsStore.qml b/ui/imports/shared/stores/DAppsStore.qml index 83b7d18e5a..dac0ad9586 100644 --- a/ui/imports/shared/stores/DAppsStore.qml +++ b/ui/imports/shared/stores/DAppsStore.qml @@ -9,12 +9,11 @@ QObject { /// \c dappsJson serialized from status-go.wallet.GetDapps signal dappsListReceived(string dappsJson) - signal userAuthenticated(string topic, string id) + signal userAuthenticated(string topic, string id, string password, string pin) signal userAuthenticationFailed(string topic, string id) - signal sessionRequestExecuted(var payload, bool success) function addWalletConnectSession(sessionJson) { - controller.addWalletConnectSession(sessionJson) + return controller.addWalletConnectSession(sessionJson) } function authenticateUser(topic, id, address) { @@ -24,9 +23,14 @@ QObject { } } - function signMessage(message) { - // TODO #14927 implement me - root.sessionRequestExecuted(message, true) + // Returns the hex encoded signature of the message or empty string if error + function signMessage(topic, id, address, password, message) { + return controller.signMessage(address, password, message) + } + + // Returns the hex encoded signature of the typedDataJson or empty string if error + function signTypedDataV4(topic, id, address, password, typedDataJson) { + return controller.signTypedDataV4(address, password, typedDataJson) } /// \c getDapps triggers an async response to \c dappsListReceived @@ -42,9 +46,9 @@ QObject { root.dappsListReceived(dappsJson) } - function onUserAuthenticationResult(topic, id, success) { + function onUserAuthenticationResult(topic, id, success, password, pin) { if (success) { - root.userAuthenticated(topic, id) + root.userAuthenticated(topic, id, password, pin) } else { root.userAuthenticationFailed(topic, id) }