feat(dapps): Add support for max fees in WalletConnect requests

Compute max fees for transaction related requests and display the
results to user.

Also:

- Add helper to convert from hex (backend) to decimal (frontend) values.
- Add helper to convert from float gwei to hex wei
- Update tests to accommodate for the new dependencies.

Sourcing of account balances is not included therefore the transaction is
allowed to go through even if the account balance is insufficient. An error
will be generated by the backend in this case.

Updates: #15192
This commit is contained in:
Stefan 2024-07-17 13:46:43 +03:00 committed by Stefan Dunca
parent e1611cbc83
commit 248ba1c1c8
12 changed files with 265 additions and 81 deletions

View File

@ -4,9 +4,12 @@ import chronicles
import app_service/service/wallet_connect/service as wallet_connect_service
import app_service/service/wallet_account/service as wallet_account_service
import helpers
logScope:
topics = "wallet-connect-controller"
QtObject:
type
Controller* = ref object of QObject
@ -44,16 +47,16 @@ QtObject:
self.dappsListReceived(res)
return true
proc userAuthenticationResult*(self: Controller, topic: string, id: string, error: bool, password: string, pin: string) {.signal.}
proc userAuthenticationResult*(self: Controller, topic: string, id: string, error: bool, password: string, pin: string, payload: 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.} =
proc authenticateUser*(self: Controller, topic: string, id: string, address: string, payload: string): bool {.slot.} =
let acc = self.walletAccountService.getAccountByAddress(address)
if acc.keyUid == "":
return false
return self.service.authenticateUser(acc.keyUid, proc(password: string, pin: string, success: bool) =
self.userAuthenticationResult(topic, id, success, password, pin)
self.userAuthenticationResult(topic, id, success, password, pin, payload)
)
proc signMessageUnsafe*(self: Controller, address: string, password: string, message: string): string {.slot.} =
@ -73,3 +76,22 @@ QtObject:
proc getEstimatedTime(self: Controller, chainId: int, maxFeePerGasHex: string): int {.slot.} =
return self.service.getEstimatedTime(chainId, maxFeePerGasHex).int
proc getSuggestedFeesJson(self: Controller, chainId: int): string {.slot.} =
let dto = self.service.getSuggestedFees(chainId)
return dto.toJson()
proc hexToDecBigString*(self: Controller, hex: string): string {.slot.} =
try:
return hexToDec(hex)
except Exception as e:
error "Failed to convert hex big int: ", hex=hex, ex=e.msg
return ""
# Convert from float gwei to hex wei
proc convertFeesInfoToHex*(self: Controller, feesInfoJson: string): string {.slot.} =
try:
return convertFeesInfoToHex(feesInfoJson)
except Exception as e:
error "Failed to convert fees info to hex: ", feesInfoJson=feesInfoJson, ex=e.msg
return ""

View File

@ -0,0 +1,21 @@
import stint, json, strutils
proc hexToDec*(hex: string): string =
return stint.parse(hex, UInt256, 16).toString()
proc convertFeesInfoToHex*(feesInfoJson: string): string =
let parsedJson = parseJson(feesInfoJson)
let maxFeeFloat = parsedJson["maxFeePerGas"].getFloat()
let maxFeeWei = int64(maxFeeFloat * 1e9)
let maxPriorityFeeFloat = parsedJson["maxPriorityFeePerGas"].getFloat()
let maxPriorityFeeWei = int64(maxPriorityFeeFloat * 1e9)
# Assemble the JSON and return it
var resultJson = %* {
"maxFeePerGas": "0x" & toHex(maxFeeWei).strip(chars = {'0'}, trailing = false),
"maxPriorityFeePerGas": "0x" & toHex(maxPriorityFeeWei).strip(chars = {'0'}, trailing = false)
}
return $resultJson

View File

@ -225,3 +225,6 @@ QtObject:
let maxFeePerGasInt = parseHexInt(maxFeePerGasHex)
maxFeePerGas = maxFeePerGasInt.float
return self.transactions.getEstimatedTime(chainId, $(maxFeePerGas))
proc getSuggestedFees*(self: Service, chainId: int): SuggestedFeesDto =
return self.transactions.suggestedFees(chainId)

View File

@ -377,6 +377,13 @@ Item {
function getEstimatedTime(chainId, maxFeePerGas) {
return Constants.TransactionEstimatedTime.LessThanThreeMins
}
function hexToDec(hex) {
if (hex.length > "0xfffffffffffff".length) {
console.warn(`Beware of possible loss of precision converting ${hex}`)
}
return parseInt(hex, 16).toString()
}
}
walletRootStore: QObject {
@ -390,6 +397,7 @@ Item {
function getNetworkShortNames(chainIds) {
return "eth:oeth:arb"
}
readonly property CurrenciesStore currencyStore: CurrenciesStore {}
}
onDisplayToastMessage: (message, isErr) => {

View File

@ -155,6 +155,7 @@ Item {
id: dappsRequestHandlerComponent
DAppsRequestHandler {
currenciesStore: CurrenciesStore {}
}
}

View File

@ -2,8 +2,19 @@ import unittest
import app/modules/main/wallet_section/poc_wallet_connect/helpers
import app/modules/shared_modules/wallet_connect/helpers
suite "wallet connect":
test "hexToDec":
check(hexToDec("0x3") == "3")
check(hexToDec("f") == "15")
test "convertFeesInfoToHex":
const feesInfoJson = "{\"maxFees\":\"24528.282681\",\"maxFeePerGas\":1.168013461,\"maxPriorityFeePerGas\":0.036572351,\"gasPrice\":\"1.168013461\"}"
check(convertFeesInfoToHex(feesInfoJson) == """{"maxFeePerGas":"0x459E7895","maxPriorityFeePerGas":"0x22E0CBF"}""")
test "parse deep link url":
const testUrl = "https://status.app/wc?uri=wc%3Aa4f32854428af0f5b6635fb7a3cb2cfe174eaad63b9d10d52ef1c686f8eab862%402%3Frelay-protocol%3Dirn%26symKey%3D4ccbae2b4c81c26fbf4a6acee9de2771705d467de9a1d24c80240e8be59de6be"

View File

@ -3,6 +3,7 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import SortFilterProxyModel 0.2
import AppLayouts.Wallet 1.0
@ -159,6 +160,8 @@ DappsComboBox {
loginType: request.account.migragedToKeycard ? Constants.LoginType.Keycard : root.loginType
visible: true
property var feesInfo: null
dappUrl: request.dappUrl
dappIcon: request.dappIcon
dappName: request.dappName
@ -180,7 +183,7 @@ DappsComboBox {
enoughFundsForTransaction: request.enoughFunds
enoughFundsForFees: request.enoughFunds
signingTransaction: request.method === SessionRequest.methods.signTransaction.name || request.method === SessionRequest.methods.sendTransaction.name
signingTransaction: !!request.method && (request.method === SessionRequest.methods.signTransaction.name || request.method === SessionRequest.methods.sendTransaction.name)
requestPayload: {
switch(request.method) {
case SessionRequest.methods.personalSign.name:
@ -219,8 +222,9 @@ DappsComboBox {
console.error("Error signing: request is null")
return
}
requestHandled = true
root.wcService.requestHandler.authenticate(request)
root.wcService.requestHandler.authenticate(request, JSON.stringify(feesInfo))
}
onRejected: {
@ -240,22 +244,18 @@ DappsComboBox {
Connections {
target: root.wcService.requestHandler
function onMaxFeesUpdated(maxFees, maxFeesWei, haveEnoughFunds, symbol) {
fiatFees = maxFees
currentCurrency = symbol
var ethStr = "?"
try {
ethStr = globalUtils.wei2Eth(maxFeesWei, 9)
} catch (e) {
// ignore error in case of tests and storybook where we don't have access to globalUtils
function onMaxFeesUpdated(fiatMaxFees, ethMaxFees, haveEnoughFunds, haveEnoughFees, symbol, feesInfo) {
dappRequestModal.hasFees = !!ethMaxFees
dappRequestModal.feesLoading = !dappRequestModal.hasFees
if (!hasFees) {
return
}
cryptoFees = ethStr
enoughFundsForTransaction = haveEnoughFunds
enoughFundsForFees = haveEnoughFunds
feesLoading = false
hasFees = !!maxFees
dappRequestModal.fiatFees = fiatMaxFees.toString()
dappRequestModal.cryptoFees = ethMaxFees.toString()
dappRequestModal.currentCurrency = symbol
dappRequestModal.enoughFundsForTransaction = haveEnoughFunds
dappRequestModal.enoughFundsForFees = haveEnoughFees
dappRequestModal.feesInfo = feesInfo
}
function onEstimatedTimeUpdated(estimatedTimeEnum) {
@ -273,6 +273,8 @@ DappsComboBox {
sessionRequestLoader.active = false
} else {
// TODO #14762 handle the error case
let userRejected = false
root.wcService.requestHandler.rejectSessionRequest(request, userRejected)
}
}
}

View File

@ -3,20 +3,21 @@ import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import StatusQ.Core.Utils 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0
import utils 1.0
import "types"
QObject {
SQUtils.QObject {
id: root
required property WalletConnectSDKBase sdk
required property DAppsStore store
required property var accountsModel
required property var networksModel
required property CurrenciesStore currenciesStore
property alias requestsModel: requests
@ -26,14 +27,14 @@ QObject {
}
/// 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)
function authenticate(request, payload) {
return store.authenticateUser(request.topic, request.id, request.account.address, payload)
}
signal sessionRequest(SessionRequestResolved request)
signal displayToastMessage(string message, bool error)
signal sessionRequestResult(/*model entry of SessionRequestResolved*/ var request, bool isSuccess)
signal maxFeesUpdated(real maxFees, int maxFeesWei, bool haveEnoughFunds, string symbol)
signal maxFeesUpdated(real fiatMaxFees, var /* Big */ ethMaxFees, bool haveEnoughFunds, bool haveEnoughFees, string symbol, var feesInfo)
// Reports Constants.TransactionEstimatedTime values
signal estimatedTimeUpdated(int estimatedTimeEnum)
@ -51,7 +52,7 @@ QObject {
}
function onSessionRequestUserAnswerResult(topic, id, accept, error) {
var request = requests.findRequest(topic, id)
let request = requests.findRequest(topic, id)
if (request === null) {
console.error("Error finding event for topic", topic, "id", id)
return
@ -85,17 +86,17 @@ QObject {
Connections {
target: root.store
function onUserAuthenticated(topic, id, password, pin) {
function onUserAuthenticated(topic, id, password, pin, payload) {
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)
d.executeSessionRequest(request, password, pin, payload)
}
function onUserAuthenticationFailed(topic, id) {
var request = requests.findRequest(topic, id)
let request = requests.findRequest(topic, id)
let methodStr = SessionRequest.methodToUserString(request.method)
if (request === null || !methodStr) {
return
@ -108,7 +109,7 @@ QObject {
}
}
QObject {
SQUtils.QObject {
id: d
function resolveAsync(event) {
@ -128,6 +129,7 @@ QObject {
console.error("Error in event data lookup", JSON.stringify(event))
return null
}
let enoughFunds = !d.isTransactionMethod(method)
let obj = sessionRequestComponent.createObject(null, {
event,
topic: event.topic,
@ -138,7 +140,7 @@ QObject {
data,
maxFeesText: "?",
maxFeesEthText: "?",
enoughFunds: false,
enoughFunds: enoughFunds,
})
if (obj === null) {
console.error("Error creating SessionRequestResolved for event")
@ -159,20 +161,16 @@ QObject {
obj.resolveDappInfoFromSession(session)
root.sessionRequest(obj)
let estimatedTimeEnum = getEstimatedTimeInterval(data, method, obj.network.chainId)
root.estimatedTimeUpdated(estimatedTimeEnum)
// TODO #15192: update maxFees
if (!event.params.request.params[0].gasLimit || !event.params.request.params[0].gasPrice) {
root.maxFeesUpdated(0, 0, true, "")
if (!d.isTransactionMethod(method)) {
return
}
let gasLimit = parseFloat(parseInt(event.params.request.params[0].gasLimit, 16));
let gasPrice = parseFloat(parseInt(event.params.request.params[0].gasPrice, 16));
let maxFees = gasLimit * gasPrice
root.maxFeesUpdated(maxFees/1000000000, maxFees, true, "Gwei")
let estimatedTimeEnum = getEstimatedTimeInterval(data, method, obj.network.chainId)
root.estimatedTimeUpdated(estimatedTimeEnum)
let st = getEstimatedFeesStatus(data, method, obj.network.chainId)
root.maxFeesUpdated(st.fiatMaxFees, st.maxFeesEth, st.haveEnoughFunds, st.haveEnoughFees, st.symbol, st.feesInfo)
})
return obj
@ -180,7 +178,7 @@ QObject {
/// Returns null if the account is not found
function lookupAccountFromEvent(event, method) {
var address = ""
let address = ""
if (method === SessionRequest.methods.personalSign.name) {
if (event.params.request.params.length < 2) {
return null
@ -198,14 +196,13 @@ QObject {
return null
}
address = event.params.request.params[0]
} else if (method === SessionRequest.methods.signTransaction.name
|| method === SessionRequest.methods.sendTransaction.name) {
} else if (d.isTransactionMethod(method)) {
if (event.params.request.params.length == 0) {
return null
}
address = event.params.request.params[0].from
}
return ModelUtils.getFirstModelEntryIf(root.accountsModel, (account) => {
return SQUtils.ModelUtils.getFirstModelEntryIf(root.accountsModel, (account) => {
return account.address.toLowerCase() === address.toLowerCase();
})
}
@ -216,7 +213,7 @@ QObject {
return null
}
let chainId = Helpers.chainIdFromEip155(event.params.chainId)
return ModelUtils.getByKey(root.networksModel, "chainId", chainId)
return SQUtils.ModelUtils.getByKey(root.networksModel, "chainId", chainId)
}
function extractMethodData(event, method) {
@ -226,7 +223,7 @@ QObject {
if (event.params.request.params.length < 1) {
return null
}
var message = ""
let message = ""
let messageIndex = (method === SessionRequest.methods.personalSign.name ? 0 : 1)
let messageParam = event.params.request.params[messageIndex]
// There is no standard on how data is encoded. Therefore we support hex or utf8
@ -275,14 +272,14 @@ QObject {
})
}
function executeSessionRequest(request, password, pin) {
function executeSessionRequest(request, password, pin, payload) {
if (!SessionRequest.getSupportedMethods().includes(request.method)) {
console.error("Unsupported method to execute: ", request.method)
return
}
if (password !== "") {
var actionResult = ""
let actionResult = ""
if (request.method === SessionRequest.methods.sign.name) {
actionResult = store.signMessageUnsafe(request.topic, request.id,
request.account.address, password,
@ -299,15 +296,36 @@ QObject {
request.account.address, password,
SessionRequest.methods.signTypedData.getMessageFromData(request.data),
request.network.chainId, legacy)
} else if (request.method === SessionRequest.methods.signTransaction.name) {
let txObj = SessionRequest.methods.signTransaction.getTxObjFromData(request.data)
actionResult = store.signTransaction(request.topic, request.id,
request.account.address, request.network.chainId, password, txObj)
} else if (request.method === SessionRequest.methods.sendTransaction.name) {
let txObj = SessionRequest.methods.sendTransaction.getTxObjFromData(request.data)
actionResult = store.sendTransaction(request.topic, request.id,
request.account.address, request.network.chainId, password, txObj)
} else if (d.isTransactionMethod(request.method)) {
let txObj = d.getTxObject(request.method, request.data)
if (!!payload) {
let feesInfoJson = payload
let hexFeesJson = root.store.convertFeesInfoToHex(feesInfoJson)
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) {
actionResult = store.signTransaction(request.topic, request.id,
request.account.address, request.network.chainId, password, txObj)
} else if (request.method === SessionRequest.methods.sendTransaction.name) {
let txObj = SessionRequest.methods.sendTransaction.getTxObjFromData(request.data)
actionResult = store.sendTransaction(request.topic, request.id,
request.account.address, request.network.chainId, password, txObj)
}
}
let isSuccessful = (actionResult != "")
if (isSuccessful) {
// acceptSessionRequest will trigger an sdk.sessionRequestUserAnswerResult signal
@ -324,28 +342,110 @@ QObject {
// Returns Constants.TransactionEstimatedTime
function getEstimatedTimeInterval(data, method, chainId) {
if (method != SessionRequest.methods.signTransaction.name
&& method != SessionRequest.methods.sendTransaction.name)
{
return ""
let tx = {}
let maxFeePerGas = ""
if (d.isTransactionMethod(method)) {
tx = d.getTxObject(method, data)
// Empty string instructs getEstimatedTime to fetch the blockchain value
if (!!tx.maxFeePerGas) {
maxFeePerGas = tx.maxFeePerGas
} else if (!!tx.gasPrice) {
maxFeePerGas = tx.gasPrice
}
}
var tx = {}
return root.store.getEstimatedTime(chainId, maxFeePerGas)
}
// Returns {
// maxFees -> Big number in Gwei
// maxFeePerGas
// maxPriorityFeePerGas
// gasPrice
// }
function getEstimatedMaxFees(data, method, chainId) {
let tx = {}
if (d.isTransactionMethod(method)) {
tx = d.getTxObject(method, data)
}
let Math = SQUtils.AmountsArithmetic
let gasLimit = Math.fromString("21000")
let gasPrice, maxFeePerGas, maxPriorityFeePerGas
// Beware, the tx values are standard blockchain hex big number values; the fees values are nim's float64 values, hence the complex conversions
if (!!tx.maxFeePerGas && !!tx.maxPriorityFeePerGas) {
let maxFeePerGasDec = root.store.hexToDec(tx.maxFeePerGas)
gasPrice = Math.fromString(maxFeePerGasDec)
// Source fees info from the incoming transaction for when we process it
maxFeePerGas = maxFeePerGasDec
let maxPriorityFeePerGasDec = root.store.hexToDec(tx.maxPriorityFeePerGas)
maxPriorityFeePerGas = maxPriorityFeePerGasDec
} else {
let fees = root.store.getSuggestedFees(chainId)
maxPriorityFeePerGas = fees.maxPriorityFeePerGas
if (fees.eip1559Enabled) {
if (!!fees.maxFeePerGasM) {
gasPrice = Math.fromString(fees.maxFeePerGasM)
maxFeePerGas = fees.maxFeePerGasM
} else if(!!tx.maxFeePerGas) {
let maxFeePerGasDec = root.store.hexToDec(tx.maxFeePerGas)
gasPrice = Math.fromString(maxFeePerGasDec)
maxFeePerGas = maxFeePerGasDec
} else {
console.error("Error fetching maxFeePerGas from fees or tx objects")
return
}
} else {
if (!!fees.gasPrice) {
gasPrice = Math.fromString(fees.gasPrice)
} else {
console.error("Error fetching suggested fees")
return
}
}
gasPrice = Math.sum(gasPrice, Math.fromString(fees.l1GasFee))
}
let maxFees = Math.times(gasLimit, gasPrice)
return {maxFees, maxFeePerGas, maxPriorityFeePerGas, gasPrice}
}
function getEstimatedFeesStatus(data, method, chainId) {
let Math = SQUtils.AmountsArithmetic
let feesInfo = getEstimatedMaxFees(data, method, chainId)
let maxFeesEth = Math.div(feesInfo.maxFees, Math.fromString("1000000000"))
// TODO #15192: extract account.balance
//let accountFundsEth = account.balance
//let haveEnoughFees = Math.cmp(accountFundsEth, maxFeesEth) >= 0
let haveEnoughFees = true
let maxFeesEthStr = maxFeesEth.toString()
let fiatMaxFees = root.currenciesStore.getFiatValue(maxFeesEthStr, Constants.ethToken)
let symbol = root.currenciesStore.currentCurrency
// We don't process the transaction so we don't have this information yet
let haveEnoughFunds = true
return {fiatMaxFees, maxFeesEth, haveEnoughFunds, haveEnoughFees, symbol, feesInfo}
}
function isTransactionMethod(method) {
return method === SessionRequest.methods.signTransaction.name
|| method === SessionRequest.methods.sendTransaction.name
}
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")
}
// Empty string instructs getEstimatedTime to fetch the blockchain value
var maxFeePerGas = ""
if (!!tx.maxFeePerGas) {
maxFeePerGas = tx.maxFeePerGas
} else if (!!tx.gasPrice) {
maxFeePerGas = tx.gasPrice
}
return root.store.getEstimatedTime(chainId, maxFeePerGas)
return tx
}
}

View File

@ -238,6 +238,7 @@ QObject {
store: root.store
accountsModel: root.validAccounts
networksModel: root.flatNetworks
currenciesStore: root.walletRootStore.currencyStore
onSessionRequest: (request) => {
timeoutTimer.stop()

View File

@ -28,9 +28,9 @@ QObject {
readonly property alias dappUrl: d.dappUrl
readonly property alias dappIcon: d.dappIcon
readonly property string maxFeesText: ""
readonly property string maxFeesEthText: ""
readonly property bool enoughFunds: false
property string maxFeesText: ""
property string maxFeesEthText: ""
property bool enoughFunds: false
function resolveDappInfoFromSession(session) {
let meta = session.peer.metadata

View File

@ -8,7 +8,7 @@ QObject {
required property var controller
/// \c dappsJson serialized from status-go.wallet.GetDapps
signal dappsListReceived(string dappsJson)
signal userAuthenticated(string topic, string id, string password, string pin)
signal userAuthenticated(string topic, string id, string password, string pin, string payload)
signal userAuthenticationFailed(string topic, string id)
function addWalletConnectSession(sessionJson) {
@ -23,8 +23,8 @@ QObject {
return controller.updateSessionsMarkedAsActive(activeTopicsJson)
}
function authenticateUser(topic, id, address) {
let ok = controller.authenticateUser(topic, id, address)
function authenticateUser(topic, id, address, payload) {
let ok = controller.authenticateUser(topic, id, address, payload)
if(!ok) {
root.userAuthenticationFailed()
}
@ -70,6 +70,12 @@ QObject {
return controller.getEstimatedTime(chainId, maxFeePerGasHex)
}
// Returns nim's SuggestedFeesDto; see src/app_service/service/transaction/dto.nim
// Returns all value initialized to 0 if error
function getSuggestedFees(chainId) {
return JSON.parse(controller.getSuggestedFeesJson(chainId))
}
// Returns the hex encoded signature of the transaction or empty string if error
function signTransaction(topic, id, address, chainId, password, txObj) {
let tx = prepareTxForStatusGo(txObj)
@ -87,6 +93,15 @@ QObject {
return controller.getDapps()
}
function hexToDec(hex) {
return controller.hexToDecBigString(hex)
}
// Return just the modified fields { "maxFeePerGas": "0x<...>", "maxPriorityFeePerGas": "0x<...>" }
function convertFeesInfoToHex(feesInfoJson) {
return controller.convertFeesInfoToHex(feesInfoJson)
}
// Handle async response from controller
Connections {
target: controller
@ -95,9 +110,9 @@ QObject {
root.dappsListReceived(dappsJson)
}
function onUserAuthenticationResult(topic, id, success, password, pin) {
function onUserAuthenticationResult(topic, id, success, password, pin, payload) {
if (success) {
root.userAuthenticated(topic, id, password, pin)
root.userAuthenticated(topic, id, password, pin, payload)
} else {
root.userAuthenticationFailed(topic, id)
}