import QtQuick 2.15 import AppLayouts.Wallet.services.dapps 1.0 import AppLayouts.Wallet.services.dapps.types 1.0 import AppLayouts.Wallet.stores 1.0 as WalletStore import StatusQ.Core.Utils 0.1 as SQUtils import shared.stores 1.0 import utils 1.0 import "types" SQUtils.QObject { id: root required property WalletConnectSDKBase sdk required property DAppsStore store required property var accountsModel required property var networksModel required property CurrenciesStore currenciesStore required property WalletStore.WalletAssetsStore assetsStore property alias requestsModel: requests function rejectSessionRequest(request, userRejected) { let error = userRejected ? false : true sdk.rejectSessionRequest(request.topic, request.id, error) } /// Beware, it will fail if called multiple times before getting an answer 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 fiatMaxFees, var /* Big */ ethMaxFees, bool haveEnoughFunds, bool haveEnoughFees, string symbol, var feesInfo) // Reports Constants.TransactionEstimatedTime values signal estimatedTimeUpdated(int estimatedTimeEnum) Connections { target: sdk function onSessionRequestEvent(event) { let obj = d.resolveAsync(event) if (obj === null) { let error = true sdk.rejectSessionRequest(event.topic, event.id, error) return } requests.enqueue(obj) } function onSessionRequestUserAnswerResult(topic, id, accept, error) { let request = requests.findRequest(topic, id) if (request === null) { console.error("Error finding event for topic", topic, "id", id) return } let methodStr = SessionRequest.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) 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.sessionRequestResult(request, true /*isSuccessful*/) }) } } Connections { target: root.store 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, payload) } function onUserAuthenticationFailed(topic, id) { let request = requests.findRequest(topic, id) let methodStr = SessionRequest.methodToUserString(request.method) if (request === null || !methodStr) { return } d.lookupSession(topic, function(session) { if (session === null) return root.displayToastMessage(qsTr("Failed to authenticate %1 from %2").arg(methodStr).arg(session.peer.metadata.url), true) }) } } SQUtils.QObject { id: d function resolveAsync(event) { let method = event.params.request.method let account = lookupAccountFromEvent(event, method) if(!account) { console.error("Error finding account for event", JSON.stringify(event)) return null } let network = lookupNetworkFromEvent(event, method) if(!network) { console.error("Error finding network for event", JSON.stringify(event)) return null } let data = extractMethodData(event, method) if(!data) { console.error("Error in event data lookup", JSON.stringify(event)) return null } let enoughFunds = !d.isTransactionMethod(method) let obj = sessionRequestComponent.createObject(null, { event, topic: event.topic, id: event.id, method, account, network, data, maxFeesText: "?", maxFeesEthText: "?", enoughFunds: enoughFunds, }) if (obj === null) { console.error("Error creating SessionRequestResolved for event") return null } // Check later to have a valid request object if (!SessionRequest.getSupportedMethods().includes(method)) { console.error("Unsupported method", method) return null } 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) if (!d.isTransactionMethod(method)) { return } let estimatedTimeEnum = getEstimatedTimeInterval(data, method, obj.network.chainId) root.estimatedTimeUpdated(estimatedTimeEnum) const mainNet = lookupMainnetNetwork() let mainChainId = obj.network.chainId if (!!mainNet) { mainChainId = mainNet.chainId } else { console.error("Error finding mainnet network") } let st = getEstimatedFeesStatus(data, method, obj.network.chainId, mainChainId) let fundsStatus = checkFundsStatus(st.feesInfo.maxFees, st.feesInfo.l1GasFee, account.address, obj.network.chainId, mainNet.chainId) root.maxFeesUpdated(st.fiatMaxFees.toNumber(), st.maxFeesEth, fundsStatus.haveEnoughFunds, fundsStatus.haveEnoughForFees, st.symbol, st.feesInfo) }) return obj } /// Returns null if the account is not found function lookupAccountFromEvent(event, method) { let address = "" if (method === SessionRequest.methods.personalSign.name) { if (event.params.request.params.length < 2) { return null } address = event.params.request.params[1] } else if (method === SessionRequest.methods.sign.name) { if (event.params.request.params.length === 1) { return null } address = event.params.request.params[0] } else if(method === SessionRequest.methods.signTypedData_v4.name || method === SessionRequest.methods.signTypedData.name) { if (event.params.request.params.length < 2) { return null } address = event.params.request.params[0] } else if (d.isTransactionMethod(method)) { if (event.params.request.params.length == 0) { return null } address = event.params.request.params[0].from } return SQUtils.ModelUtils.getFirstModelEntryIf(root.accountsModel, (account) => { return account.address.toLowerCase() === address.toLowerCase(); }) } /// Returns null if the network is not found function lookupNetworkFromEvent(event, method) { if (SessionRequest.getSupportedMethods().includes(method) === false) { return null } let chainId = Helpers.chainIdFromEip155(event.params.chainId) return SQUtils.ModelUtils.getByKey(root.networksModel, "chainId", chainId) } /// Returns null if the network is not found function lookupMainnetNetwork() { return SQUtils.ModelUtils.getByKey(root.networksModel, "layer", 1) } function extractMethodData(event, method) { if (method === SessionRequest.methods.personalSign.name || method === SessionRequest.methods.sign.name) { if (event.params.request.params.length < 1) { return null } let message = "" 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 if (Helpers.isHex(messageParam)) { message = Helpers.hexToString(messageParam) } else { message = messageParam } return SessionRequest.methods.personalSign.buildDataObject(message) } else if (method === SessionRequest.methods.signTypedData_v4.name || method === SessionRequest.methods.signTypedData.name) { if (event.params.request.params.length < 2) { return null } let jsonMessage = event.params.request.params[1] let methodObj = method === SessionRequest.methods.signTypedData_v4.name ? SessionRequest.methods.signTypedData_v4 : SessionRequest.methods.signTypedData return methodObj.buildDataObject(jsonMessage) } else if (method === SessionRequest.methods.signTransaction.name) { if (event.params.request.params.length == 0) { return null } let tx = event.params.request.params[0] return SessionRequest.methods.signTransaction.buildDataObject(tx) } else if (method === SessionRequest.methods.sendTransaction.name) { if (event.params.request.params.length == 0) { return null } let tx = event.params.request.params[0] return SessionRequest.methods.sendTransaction.buildDataObject(tx) } else { return null } } function lookupSession(topicToLookup, callback) { sdk.getActiveSessions((res) => { Object.keys(res).forEach((topic) => { if (topic === topicToLookup) { let session = res[topic] callback(session) } }) }) } function executeSessionRequest(request, password, pin, payload) { if (!SessionRequest.getSupportedMethods().includes(request.method)) { console.error("Unsupported method to execute: ", request.method) return } if (password !== "") { let actionResult = "" if (request.method === SessionRequest.methods.sign.name) { actionResult = store.signMessageUnsafe(request.topic, request.id, request.account.address, password, SessionRequest.methods.personalSign.getMessageFromData(request.data)) } else if (request.method === SessionRequest.methods.personalSign.name) { actionResult = store.signMessage(request.topic, request.id, request.account.address, password, SessionRequest.methods.personalSign.getMessageFromData(request.data)) } else if (request.method === SessionRequest.methods.signTypedData_v4.name || request.method === SessionRequest.methods.signTypedData.name) { let legacy = request.method === SessionRequest.methods.signTypedData.name actionResult = store.safeSignTypedData(request.topic, request.id, request.account.address, password, SessionRequest.methods.signTypedData.getMessageFromData(request.data), request.network.chainId, legacy) } 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) { 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 sdk.acceptSessionRequest(request.topic, request.id, actionResult) } else { root.sessionRequestResult(request, isSuccessful) } } else if (pin !== "") { console.debug("TODO #15097 sign message using keycard: ", request.data) } else { console.error("No password or pin provided to sign message") } } // Returns Constants.TransactionEstimatedTime function getEstimatedTimeInterval(data, method, chainId) { 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 } } return root.store.getEstimatedTime(chainId, maxFeePerGas) } // Returns { // maxFees -> Big number in Gwei // maxFeePerGas // maxPriorityFeePerGas // gasPrice // } function getEstimatedMaxFees(data, method, chainId, mainNetChainId) { let tx = {} if (d.isTransactionMethod(method)) { tx = d.getTxObject(method, data) } let Math = SQUtils.AmountsArithmetic let gasLimit = Math.fromString("21000") let gasPrice, maxFeePerGas, maxPriorityFeePerGas let l1GasFee = Math.fromNumber(0) // 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.fromNumber(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.fromNumber(fees.gasPrice) } else { console.error("Error fetching suggested fees") return } } l1GasFee = Math.fromNumber(fees.l1GasFee) } let maxFees = Math.times(gasLimit, gasPrice) return {maxFees, maxFeePerGas, maxPriorityFeePerGas, gasPrice, l1GasFee} } // Returned values are Big numbers function getEstimatedFeesStatus(data, method, chainId, mainNetChainId) { let Math = SQUtils.AmountsArithmetic let feesInfo = getEstimatedMaxFees(data, method, chainId, mainNetChainId) let totalMaxFees = Math.sum(feesInfo.maxFees, feesInfo.l1GasFee) let maxFeesEth = Math.div(totalMaxFees, Math.fromString("1000000000")) let maxFeesEthStr = maxFeesEth.toString() let fiatMaxFeesStr = root.currenciesStore.getFiatValue(maxFeesEthStr, Constants.ethToken) let fiatMaxFees = Math.fromString(fiatMaxFeesStr) let symbol = root.currenciesStore.currentCurrencySymbol return {fiatMaxFees, maxFeesEth, symbol, feesInfo} } function checkBalanceForChain(balances, address, chainId, fees) { let Math = SQUtils.AmountsArithmetic let accEth = SQUtils.ModelUtils.getFirstModelEntryIf(balances, (balance) => { return balance.account.toLowerCase() === address.toLowerCase() && balance.chainId === chainId }) if (!accEth) { console.error("Error balance lookup for account ", address, " on chain ", chainId) return {haveEnoughForFees, haveEnoughFunds} } let accountFundsWei = Math.fromString(accEth.balance) let accountFundsEth = Math.div(accountFundsWei, Math.fromString("1000000000000000000")) let feesEth = Math.div(fees, Math.fromString("1000000000")) return Math.cmp(accountFundsEth, feesEth) >= 0 } function checkFundsStatus(maxFees, l1GasFee, address, chainId, mainNetChainId) { let Math = SQUtils.AmountsArithmetic let haveEnoughForFees = false // TODO #15192: extract funds from transaction and check against it let haveEnoughFunds = true let token = SQUtils.ModelUtils.getByKey(root.assetsStore.groupedAccountAssetsModel, "tokensKey", Constants.ethToken) if (!token || !token.balances) { console.error("Error token balances lookup for ETH") return {haveEnoughForFees, haveEnoughFunds} } if (chainId == mainNetChainId) { const finalFees = Math.sum(maxFees, l1GasFee) haveEnoughForFees = checkBalanceForChain(token.balances, address, chainId, finalFees) } else { const haveEnoughOnChain = checkBalanceForChain(token.balances, address, chainId, maxFees) const haveEnoughOnMain = checkBalanceForChain(token.balances, address, mainNetChainId, l1GasFee) haveEnoughForFees = haveEnoughOnChain && haveEnoughOnMain } return {haveEnoughForFees, haveEnoughFunds} } 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") } return tx } } /// The queue is used to ensure that the events are processed in the order they are received but they could be /// processed handled randomly on user intervention through activity center SessionRequestsModel { id: requests } Component { id: sessionRequestComponent SessionRequestResolved { } } }