import QtQuick 2.15 import AppLayouts.Wallet.services.dapps 1.0 import AppLayouts.Wallet.services.dapps.plugins 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 SQUtils.QObject { id: root required property WalletConnectSDKBase sdk required property DAppsStore store required property var accountsModel // Required roles: chainId, layer, isOnline required property var networksModel required property CurrenciesStore currenciesStore required property WalletStore.WalletAssetsStore assetsStore property alias requestsModel: requests readonly property bool isServiceOnline: chainsSupervisorPlugin.anyChainAvailable && sdk.sdkReady function subscribeForFeeUpdates(topic, id) { d.subscribeForFeeUpdates(topic, id) } function pair(uri) { return sdk.pair(uri) } /// Approves or rejects the session proposal function approvePairSession(key, approvedChainIds, accountAddress) { const approvedNamespaces = JSON.parse( DAppsHelpers.buildSupportedNamespaces(approvedChainIds, [accountAddress], SessionRequest.getSupportedMethods()) ) if (siwePlugin.connectionApproved(key, approvedNamespaces)) { return } if (!d.activeProposals.has(key)) { console.error("No active proposal found for key: " + key) return } const proposal = d.activeProposals.get(key) d.acceptedSessionProposal = proposal d.acceptedNamespaces = approvedNamespaces sdk.buildApprovedNamespaces(key, proposal.params, approvedNamespaces) } /// Rejects the session proposal function rejectPairSession(id) { if (siwePlugin.connectionRejected(id)) { return } sdk.rejectSession(id) } /// Disconnects the WC session with the given topic function disconnectSession(sessionTopic) { wcSDK.disconnectSession(sessionTopic) } function validatePairingUri(uri){ const info = DAppsHelpers.extractInfoFromPairUri(uri) sdk.getActiveSessions((sessions) => { // Check if the URI is already paired let validationState = Pairing.errors.uriOk for (const key in sessions) { if (sessions[key].pairingTopic === info.topic) { validationState = Pairing.errors.alreadyUsed break } } // Check if expired if (validationState === Pairing.errors.uriOk) { const now = (new Date().getTime())/1000 if (info.expiry < now) { validationState = Pairing.errors.expired } } root.pairingValidated(validationState) }); } signal sessionRequest(var id) /*type - maps to Constants.ephemeralNotificationType*/ signal displayToastMessage(string message, int type) signal pairingValidated(int validationState) signal pairingResponse(int state) // Maps to Pairing.errors signal connectDApp(var chains, string dAppUrl, string dAppName, string dAppIcon, var key) signal approveSessionResult(var proposalId, bool error, var topic) signal dappDisconnected(var topic, string url, bool error) Connections { target: sdk function onRejectSessionResult(proposalId, err) { if (!d.activeProposals.has(proposalId)) { console.error("No active proposal found for key: " + proposalId) return } const proposal = d.activeProposals.get(proposalId) d.activeProposals.delete(proposalId) const app_url = proposal.params.proposer.metadata.url ?? "-" const app_domain = SQUtils.StringUtils.extractDomainFromLink(app_url) if(err) { root.pairingResponse(Pairing.errors.unknownError) root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_domain), Constants.ephemeralNotificationType.danger) } else { root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_domain), Constants.ephemeralNotificationType.success) } } function onApproveSessionResult(proposalId, session, err) { if (!d.activeProposals.has(proposalId)) { console.error("No active proposal found for key: " + proposalId) return } if (!d.acceptedSessionProposal || d.acceptedSessionProposal.id !== proposalId) { console.error("No accepted proposal found for key: " + proposalId) d.activeProposals.delete(proposalId) return } const proposal = d.activeProposals.get(proposalId) d.activeProposals.delete(proposalId) d.acceptedSessionProposal = null d.acceptedNamespaces = null if (err) { root.pairingResponse(Pairing.errors.unknownError) return } // TODO #14754: implement custom dApp notification const app_url = proposal.params.proposer.metadata.url ?? "-" const app_domain = SQUtils.StringUtils.extractDomainFromLink(app_url) root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_domain), Constants.ephemeralNotificationType.success) // Persist session if(!root.store.addWalletConnectSession(JSON.stringify(session))) { console.error("Failed to persist session") } // Notify client root.approveSessionResult(proposalId, err, session.topic) } function onBuildApprovedNamespacesResult(key, approvedNamespaces, error) { if (!d.activeProposals.has(key)) { console.error("No active proposal found for key: " + key) return } if(error || !approvedNamespaces) { // Check that it contains Non conforming namespaces" if (error.includes("Non conforming namespaces")) { root.pairingResponse(Pairing.errors.unsupportedNetwork) } else { root.pairingResponse(Pairing.errors.unknownError) } return } approvedNamespaces = applyChainAgnosticFix(approvedNamespaces) if (d.acceptedSessionProposal) { sdk.approveSession(d.acceptedSessionProposal, approvedNamespaces) } else { const proposal = d.activeProposals.get(key) const res = DAppsHelpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces) const chains = res.chains const dAppUrl = proposal.params.proposer.metadata.url const dAppName = proposal.params.proposer.metadata.name const dAppIcons = proposal.params.proposer.metadata.icons const dAppIcon = dAppIcons && dAppIcons.length > 0 ? dAppIcons[0] : "" root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key) } } //Special case for chain agnostic dapps //WC considers the approved namespace as valid, but there's no chainId or account established //Usually this request is declared by using `eip155:0`, but we don't support this chainID, resulting in empty `chains` and `accounts` //The established connection will use for all user approved chains and accounts //This fix is applied to all valid namespaces that don't have a chainId or account function applyChainAgnosticFix(approvedNamespaces) { try { const an = approvedNamespaces.eip155 const chainAgnosticRequest = (!an.chains || an.chains.length === 0) && (!an.accounts || an.accounts.length === 0) if (!chainAgnosticRequest) { return approvedNamespaces } // If the `d.acceptedNamespaces` is set it means the user already confirmed the chain and account if (!!d.acceptedNamespaces) { approvedNamespaces.eip155.chains = d.acceptedNamespaces.eip155.chains approvedNamespaces.eip155.accounts = d.acceptedNamespaces.eip155.accounts return approvedNamespaces } // Show to the user all possible chains const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels( root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods()) const supportedNamespaces = JSON.parse(supportedNamespacesStr) approvedNamespaces.eip155.chains = supportedNamespaces.eip155.chains approvedNamespaces.eip155.accounts = supportedNamespaces.eip155.accounts } catch (e) { console.warn("WC Error applying chain agnostic fix", e) } return approvedNamespaces } function onSessionProposal(sessionProposal) { const key = sessionProposal.id d.activeProposals.set(key, sessionProposal) const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels( root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods()) sdk.buildApprovedNamespaces(key, sessionProposal.params, JSON.parse(supportedNamespacesStr)) } function onPairResponse(ok) { root.pairingResponse(ok) } function onSessionRequestEvent(event) { const res = d.resolveAsync(event) if (res.code == d.resolveAsyncResult.error) { let error = true sdk.rejectSessionRequest(event.topic, event.id, error) return } if (res.code == d.resolveAsyncResult.ignored) { return } if (!res.obj) { console.error("Unexpected res.obj value!") return } requests.enqueue(res.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 } const appUrl = request.dappUrl const appDomain = SQUtils.StringUtils.extractDomainFromLink(appUrl) const requestExpired = request.isExpired() requests.removeRequest(topic, id) if (error) { root.displayToastMessage(qsTr("Fail to %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger) sdk.rejectSessionRequest(topic, id, true /*hasError*/) console.error(`Error accepting session request for topic: ${topic}, id: ${id}, accept: ${accept}, error: ${error}`) return } if (!requestExpired) { let actionStr = accept ? qsTr("accepted") : qsTr("rejected") root.displayToastMessage("%1 %2 %3".arg(appDomain).arg(methodStr).arg(actionStr), Constants.ephemeralNotificationType.success) return } root.displayToastMessage("%1 sign request timed out".arg(appDomain), Constants.ephemeralNotificationType.normal) } function onSessionRequestExpired(sessionId) { // Expired event coming from WC // Handling as a failsafe in case the event is not processed by the SDK let request = requests.findById(sessionId) if (request === null) { console.error("Error finding event for session id", sessionId) return } if (request.isExpired()) { return //nothing to do. The request is already expired } request.setExpired() } function onSessionDelete(topic, err) { d.disconnectSessionRequested(topic, err) } } SiweRequestPlugin { id: siwePlugin sdk: root.sdk store: root.store accountsModel: root.accountsModel networksModel: root.networksModel onRegisterSignRequest: (request) => { requests.enqueue(request) } onUnregisterSignRequest: (requestId) => { const request = requests.findById(requestId) if (request === null) { console.error("SiweRequestPlugin::onUnregisterSignRequest: Error finding event for requestId", requestId) return } requests.removeRequest(request.topic, requestId) } onConnectDApp: (chains, dAppUrl, dAppName, dAppIcon, key) => { root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key) } onSiweFailed: (id, error, topic) => { root.approveSessionResult(id, error, topic) } onSiweSuccessful: (id, topic) => { d.lookupSession(topic, function(session) { // TODO #14754: implement custom dApp notification let meta = session.peer.metadata const dappUrl = meta.url ?? "-" const dappDomain = SQUtils.StringUtils.extractDomainFromLink(dappUrl) root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(dappDomain), Constants.ephemeralNotificationType.success) // Persist session if(!root.store.addWalletConnectSession(JSON.stringify(session))) { console.error("Failed to persist session") } root.approveSessionResult(id, "", topic) }) } } SQUtils.QObject { id: d property int selectedFeesMode: Constants.FeesMode.Medium function getFeesForFeesMode(feesObj) { if (!(feesObj.hasOwnProperty("maxFeePerGasL") && feesObj.hasOwnProperty("maxFeePerGasM") && feesObj.hasOwnProperty("maxFeePerGasH"))) { throw new Error("inappropriate fees object provided") } switch (d.selectedFeesMode) { case Constants.FeesMode.Low: return feesObj.maxFeePerGasL case Constants.FeesMode.Medium: return feesObj.maxFeePerGasM case Constants.FeesMode.High: return feesObj.maxFeePerGasH default: throw new Error("unknown selected mode") } } property var feesSubscriptions: [] function findSubscriptionIndex(topic, id) { for (let i = 0; i < d.feesSubscriptions.length; i++) { const subscription = d.feesSubscriptions[i] if (subscription.topic == topic && subscription.id == id) { return i } } return -1 } function findChainIndex(chainId) { for (let i = 0; i < feesSubscription.chainIds.length; i++) { if (feesSubscription.chainIds[i] == chainId) { return i } } return -1 } function subscribeForFeeUpdates(topic, id) { const request = requests.findRequest(topic, id) if (request === null) { console.error("Error finding event for subscribing for fees for topic", topic, "id", id) return } const index = d.findSubscriptionIndex(topic, id) if (index >= 0) { return } d.feesSubscriptions.push({ topic: topic, id: id, chainId: request.chainId }) for (let i = 0; i < feesSubscription.chainIds.length; i++) { if (feesSubscription.chainIds == request.chainId) { return } } feesSubscription.chainIds.push(request.chainId) feesSubscription.restart() } function unsubscribeForFeeUpdates(topic, id) { const index = d.findSubscriptionIndex(topic, id) if (index == -1) { return } const chainId = d.feesSubscriptions[index].chainId d.feesSubscriptions.splice(index, 1) const chainIndex = d.findChainIndex(chainId) if (index == -1) { return } let found = false for (let i = 0; i < d.feesSubscriptions.length; i++) { if (d.feesSubscriptions[i].chainId == chainId) { found = true break } } if (found) { return } feesSubscription.chainIds.splice(chainIndex, 1) if (feesSubscription.chainIds.length == 0) { feesSubscription.stop() } } readonly property QtObject resolveAsyncResult: QtObject { readonly property int error: 0 readonly property int ok: 1 readonly property int ignored: 2 } property var activeProposals: new Map() // key: proposalId, value: sessionProposal property var acceptedSessionProposal: null property var acceptedNamespaces: null // returns { // obj: obj or nil // code: resolveAsyncResult codes // } function resolveAsync(event) { const method = event.params.request.method const { accountAddress, success } = lookupAccountFromEvent(event, method) if(!success) { console.info("Error finding accountAddress for event", JSON.stringify(event)) return { obj: null, code: resolveAsyncResult.error } } if (!accountAddress) { console.info("Account not found for event", JSON.stringify(event)) return { obj: null, code: resolveAsyncResult.ignored } } let chainId = lookupNetworkFromEvent(event, method) if(!chainId) { console.error("Error finding chainId for event", JSON.stringify(event)) return { obj: null, code: resolveAsyncResult.error } } const data = extractMethodData(event, method) if(!data) { console.error("Error in event data lookup", JSON.stringify(event)) return { obj: null, code: resolveAsyncResult.error } } const interpreted = d.prepareData(method, data, chainId) const enoughFunds = !d.isTransactionMethod(method) const requestExpiry = event.params.request.expiryTimestamp let obj = sessionRequestComponent.createObject(null, { event, topic: event.topic, requestId: event.id, method, accountAddress, chainId, data, preparedData: interpreted.preparedData, maxFeesText: "?", maxFeesEthText: "?", expirationTimestamp: requestExpiry }) if (obj === null) { console.error("Error creating SessionRequestResolved for event") return { obj: null, code: resolveAsyncResult.error } } // Check later to have a valid request object if (!SessionRequest.getSupportedMethods().includes(method)) { console.error("Unsupported method", method) return { obj: null, code: resolveAsyncResult.error } } 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.requestId) d.updateFeesParamsToPassedObj(obj) }) return { obj: obj, code: resolveAsyncResult.ok } } function updateFeesParamsToPassedObj(obj) { if (!d.isTransactionMethod(obj.method)) { return } obj.estimatedTimeCategory = getEstimatedTimeInterval(obj.data, obj.method, obj.chainId) const mainNet = lookupMainnetNetwork() let mainChainId = obj.chainId if (!!mainNet) { mainChainId = mainNet.chainId } else { console.error("Error finding mainnet network") } const interpreted = d.prepareData(obj.method, obj.data, obj.chainId) let st = getEstimatedFeesStatus(obj.data, obj.method, obj.chainId, mainChainId) let fundsStatus = checkFundsStatus(st.feesInfo.maxFees, st.feesInfo.l1GasFee, obj.accountAddress, obj.chainId, mainNet.chainId, interpreted.value) obj.fiatMaxFees = st.fiatMaxFees obj.ethMaxFees = st.maxFeesEth obj.haveEnoughFunds = fundsStatus.haveEnoughFunds obj.haveEnoughFees = fundsStatus.haveEnoughForFees obj.feesInfo = st.feesInfo } /// returns { /// accountAddress /// success /// } /// if account is null and success is true it means that the account was not found function lookupAccountFromEvent(event, method) { let address = "" if (method === SessionRequest.methods.personalSign.name) { if (event.params.request.params.length < 2) { return { accountAddress: "", success: false } } address = event.params.request.params[1] } else if (method === SessionRequest.methods.sign.name) { if (event.params.request.params.length === 1) { return { accountAddress: "", success: false } } 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 { accountAddress: "", success: false } } address = event.params.request.params[0] } else if (d.isTransactionMethod(method)) { if (event.params.request.params.length == 0) { return { accountAddress: "", success: false } } address = event.params.request.params[0].from } else { console.error("Unsupported method to lookup account: ", method) return { accountAddress: "", success: false } } const account = SQUtils.ModelUtils.getFirstModelEntryIf(root.accountsModel, (account) => { return account.address.toLowerCase() === address.toLowerCase(); }) if (!account) { return { accountAddress: "", success: true } } return { accountAddress: account.address, success: true } } /// Returns null if the network is not found function lookupNetworkFromEvent(event, method) { if (SessionRequest.getSupportedMethods().includes(method) === false) { return null } const chainId = DAppsHelpers.chainIdFromEip155(event.params.chainId) const network = SQUtils.ModelUtils.getByKey(root.networksModel, "chainId", chainId) if (!network) { return null } return network.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 = "" const messageIndex = (method === SessionRequest.methods.personalSign.name ? 0 : 1) const messageParam = event.params.request.params[messageIndex] // There is no standard on how data is encoded. Therefore we support hex or utf8 if (DAppsHelpers.isHex(messageParam)) { message = DAppsHelpers.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 } const jsonMessage = event.params.request.params[1] const 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 } const 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 } const 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 false } if (password === "") { console.error("No password provided to sign message") return false } if (request.method === SessionRequest.methods.sign.name) { store.signMessageUnsafe(request.topic, request.requestId, request.accountAddress, SessionRequest.methods.personalSign.getMessageFromData(request.data), password, pin) } else if (request.method === SessionRequest.methods.personalSign.name) { store.signMessage(request.topic, request.requestId, request.accountAddress, SessionRequest.methods.personalSign.getMessageFromData(request.data), password, pin) } else if (request.method === SessionRequest.methods.signTypedData_v4.name || request.method === SessionRequest.methods.signTypedData.name) { let legacy = request.method === SessionRequest.methods.signTypedData.name store.safeSignTypedData(request.topic, request.requestId, request.accountAddress, SessionRequest.methods.signTypedData.getMessageFromData(request.data), request.chainId, legacy, password, pin) } 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) { store.signTransaction(request.topic, request.requestId, request.accountAddress, request.chainId, txObj, password, pin) } else if (request.method === SessionRequest.methods.sendTransaction.name) { store.sendTransaction( request.topic, request.requestId, request.accountAddress, request.chainId, txObj, password, pin) } } return true } // 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 BigOps = SQUtils.AmountsArithmetic let gasLimit = BigOps.fromString("21000") let gasPrice, maxFeePerGas, maxPriorityFeePerGas let l1GasFee = BigOps.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) { maxFeePerGas = hexToGwei(tx.maxFeePerGas) maxPriorityFeePerGas = hexToGwei(tx.maxPriorityFeePerGas) // TODO: check why we need to set gasPrice here and why if it's not checked we cannot send the tx and fees are unknown???? gasPrice = hexToGwei(tx.maxFeePerGas) } else { let fees = root.store.getSuggestedFees(chainId) maxPriorityFeePerGas = fees.maxPriorityFeePerGas if (fees.eip1559Enabled) { if (!!fees.maxFeePerGasM) { gasPrice = BigOps.fromNumber(fees.maxFeePerGasM) maxFeePerGas = fees.maxFeePerGasM } else if(!!tx.maxFeePerGas) { let maxFeePerGasDec = root.store.hexToDec(tx.maxFeePerGas) gasPrice = BigOps.fromString(maxFeePerGasDec) maxFeePerGas = maxFeePerGasDec } else { console.error("Error fetching maxFeePerGas from fees or tx objects") return } } else { if (!!fees.gasPrice) { gasPrice = BigOps.fromNumber(fees.gasPrice) } else { console.error("Error fetching suggested fees") return } } l1GasFee = BigOps.fromNumber(fees.l1GasFee) } let maxFees = BigOps.times(gasLimit, gasPrice) return {maxFees, maxFeePerGas, maxPriorityFeePerGas, gasPrice, l1GasFee} } // Returned values are Big numbers function getEstimatedFeesStatus(data, method, chainId, mainNetChainId) { let BigOps = SQUtils.AmountsArithmetic let feesInfo = getEstimatedMaxFees(data, method, chainId, mainNetChainId) let totalMaxFees = BigOps.sum(feesInfo.maxFees, feesInfo.l1GasFee) let maxFeesEth = BigOps.div(totalMaxFees, BigOps.fromNumber(1, 9)) let maxFeesEthStr = maxFeesEth.toString() let fiatMaxFeesStr = root.currenciesStore.getFiatValue(maxFeesEthStr, Constants.ethToken) let fiatMaxFees = BigOps.fromString(fiatMaxFeesStr) let symbol = root.currenciesStore.currentCurrency return {fiatMaxFees, maxFeesEth, symbol, feesInfo} } function getBalanceInEth(balances, address, chainId) { const BigOps = 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 null } let accountFundsWei = BigOps.fromString(accEth.balance) return BigOps.div(accountFundsWei, BigOps.fromNumber(1, 18)) } // Returns {haveEnoughForFees, haveEnoughFunds} and true in case of error not to block request function checkFundsStatus(maxFees, l1GasFee, address, chainId, mainNetChainId, valueEth) { let BigOps = SQUtils.AmountsArithmetic let haveEnoughForFees = true 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} } let chainBalance = getBalanceInEth(token.balances, address, chainId) if (!chainBalance) { console.error("Error fetching chain balance") return {haveEnoughForFees, haveEnoughFunds} } haveEnoughFunds = BigOps.cmp(chainBalance, valueEth) >= 0 if (haveEnoughFunds) { chainBalance = BigOps.sub(chainBalance, valueEth) if (chainId == mainNetChainId) { const finalFees = BigOps.sum(maxFees, l1GasFee) let feesEth = BigOps.div(finalFees, BigOps.fromNumber(1, 9)) haveEnoughForFees = BigOps.cmp(chainBalance, feesEth) >= 0 } else { const feesChain = BigOps.div(maxFees, BigOps.fromNumber(1, 9)) const haveEnoughOnChain = BigOps.cmp(chainBalance, feesChain) >= 0 const mainBalance = getBalanceInEth(token.balances, address, mainNetChainId) if (!mainBalance) { console.error("Error fetching mainnet balance") return {haveEnoughForFees, haveEnoughFunds} } const feesMain = BigOps.div(l1GasFee, BigOps.fromNumber(1, 9)) const haveEnoughOnMain = BigOps.cmp(mainBalance, feesMain) >= 0 haveEnoughForFees = haveEnoughOnChain && haveEnoughOnMain } } else { haveEnoughForFees = false } 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 } // returns { // preparedData, // value // null or ETH Big number // } function prepareData(method, data, chainId) { let payload = null switch(method) { case SessionRequest.methods.personalSign.name: { payload = SessionRequest.methods.personalSign.getMessageFromData(data) break } case SessionRequest.methods.sign.name: { payload = SessionRequest.methods.sign.getMessageFromData(data) break } case SessionRequest.methods.signTypedData_v4.name: { const stringPayload = SessionRequest.methods.signTypedData_v4.getMessageFromData(data) payload = JSON.stringify(JSON.parse(stringPayload), null, 2) break } case SessionRequest.methods.signTypedData.name: { const stringPayload = SessionRequest.methods.signTypedData.getMessageFromData(data) payload = JSON.stringify(JSON.parse(stringPayload), null, 2) break } case SessionRequest.methods.signTransaction.name: case SessionRequest.methods.sendTransaction.name: // For transactions we process the data in a different way as follows break default: console.error("Unhandled method", method) break; } let value = SQUtils.AmountsArithmetic.fromNumber(0) if (d.isTransactionMethod(method)) { let txObj = d.getTxObject(method, data) let tx = Object.assign({}, txObj) let fees = root.store.getSuggestedFees(chainId) if (tx.value) { value = hexToEth(tx.value) tx.value = value.toString() } if (tx.hasOwnProperty("maxFeePerGas")) { if (tx.maxFeePerGas) { tx.maxFeePerGas = hexToGwei(tx.maxFeePerGas).toString() } else if (fees.eip1559Enabled) { try { tx.maxFeePerGas = d.getFeesForFeesMode(fees) } catch (e) { console.warn(e) } } } if (tx.hasOwnProperty("maxPriorityFeePerGas")) { if (tx.maxPriorityFeePerGas) { tx.maxPriorityFeePerGas = hexToGwei(tx.maxPriorityFeePerGas).toString() } else if (fees.eip1559Enabled) { tx.maxPriorityFeePerGas = fees.maxPriorityFeePerGas } } if (tx.hasOwnProperty("gasPrice")) { if (tx.gasPrice) { tx.gasPrice = hexToGwei(tx.gasPrice) } else if (!fees.eip1559Enabled) { tx.gasPrice = fees.gasPrice } } if (tx.gasLimit) { tx.gasLimit = parseInt(root.store.hexToDec(tx.gasLimit)) } if (tx.nonce) { tx.nonce = parseInt(root.store.hexToDec(tx.nonce)) } payload = JSON.stringify(tx, null, 2) } return { preparedData: payload, value: value } } function hexToEth(value) { return hexToEthDenomination(value, "eth") } function hexToGwei(value) { return hexToEthDenomination(value, "gwei") } function hexToEthDenomination(value, ethUnit) { let unitMapping = { "gwei": 9, "eth": 18 } let BigOps = SQUtils.AmountsArithmetic let decValue = root.store.hexToDec(value) if (!!decValue) { return BigOps.div(BigOps.fromNumber(decValue), BigOps.fromNumber(1, unitMapping[ethUnit])) } return BigOps.fromNumber(0) } function disconnectSessionRequested(topic, err) { // Get all sessions and filter the active ones for known accounts // Act on the first matching session with the same topic const activeSessionsCallback = (allSessions, success) => { root.store.activeSessionsReceived.disconnect(activeSessionsCallback) if (!success) { // TODO #14754: implement custom dApp notification root.dappDisconnected("", "", true) return } // Convert to original format const webSdkSessions = allSessions.map((session) => { return JSON.parse(session.sessionJson) }) const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(webSdkSessions, root.accountsModel) for (const sessionID in sessions) { const session = sessions[sessionID] if (session.topic == topic) { root.store.deactivateWalletConnectSession(topic) const dappUrl = session.peer.metadata.url ?? "-" root.dappDisconnected(topic, dappUrl, err) break } } } root.store.activeSessionsReceived.connect(activeSessionsCallback) if (!root.store.getActiveSessions()) { root.store.activeSessionsReceived.disconnect(activeSessionsCallback) // TODO #14754: implement custom dApp notification } } } /// 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 SessionRequestWithAuth { id: request sourceId: Constants.DAppConnectors.WalletConnect store: root.store function signedHandler(topic, id, data) { if (topic != request.topic || id != request.requestId) { return } root.store.signingResult.disconnect(request.signedHandler) let hasErrors = (data == "") if (!hasErrors) { // acceptSessionRequest will trigger an sdk.sessionRequestUserAnswerResult signal sdk.acceptSessionRequest(topic, id, data) } else { request.reject(true) } } onAccepted: () => { d.unsubscribeForFeeUpdates(request.topic, request.requestId) } onRejected: (hasError) => { d.unsubscribeForFeeUpdates(request.topic, request.requestId) sdk.rejectSessionRequest(request.topic, request.requestId, hasError) } onAuthFailed: () => { d.unsubscribeForFeeUpdates(request.topic, request.requestId) sdk.rejectSessionRequest(request.topic, request.requestId, true) const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl) const methodStr = SessionRequest.methodToUserString(request.method) if (!methodStr) { return } root.displayToastMessage(qsTr("Failed to authenticate %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger) } onExecute: (password, pin) => { root.store.signingResult.connect(request.signedHandler) let executed = false try { executed = d.executeSessionRequest(request, password, pin, request.feesInfo) } catch (e) { console.error("Error executing session request", e) } if (!executed) { sdk.rejectSessionRequest(request.topic, request.requestId, true /*hasError*/) root.store.signingResult.disconnect(request.signedHandler) } } } } Timer { id: feesSubscription property var chainIds: [] interval: 5000 repeat: true running: Qt.application.state === Qt.ApplicationActive onTriggered: { for (let i = 0; i < chainIds.length; i++) { for (let j = 0; j < d.feesSubscriptions.length; j++) { let subscription = d.feesSubscriptions[j] if (subscription.chainId == chainIds[i]) { let request = requests.findRequest(subscription.topic, subscription.id) if (request === null) { console.error("Error updating fees for topic", subscription.topic, "id", subscription.id) continue } d.updateFeesParamsToPassedObj(request) } } } } } ChainsSupervisorPlugin { id: chainsSupervisorPlugin sdk: root.sdk networksModel: root.networksModel } }