status-desktop/ui/app/AppLayouts/Wallet/services/dapps/plugins/SignRequestPlugin.qml

671 lines
26 KiB
QML

import QtQuick 2.15
import QtQml 2.15
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import StatusQ 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0
import utils 1.0
/// Plugin that listens for session requests and manages the lifecycle of the request.
SQUtils.QObject {
id: root
required property WalletConnectSDKBase sdk
required property DAppsStore store
/// Expected to have the following roles:
/// - topic
/// - name
/// - url
/// - iconUrl
/// - rawSessions
required property var dappsModel
/// Expected to have the following roles:
/// - tokensKey
/// - balances
required property var groupedAccountAssetsModel
/// Expected to have the following roles:
/// - layer
/// - chainId
required property var networksModel
/// Expected to have the following roles:
/// - address
required property var accountsModel
/// App currency
required property string currentCurrency
// SessionRequestsModel where the requests are stored
// This component will append and remove requests from this model
required property SessionRequestsModel requests
// Function to transform the eth value to fiat
property var getFiatValue: (maxFeesEthStr, token /*Constants.ethToken*/) => console.error("getFiatValue not implemented")
// Signals
/// Signal emitted when a session request is accepted
signal accepted(string topic, string id, string data)
/// Signal emitted when a session request is rejected
signal rejected(string topic, string id, bool hasError)
/// Signal emitted when a session request is completed
/// Completed mean that we have the ACK from the SDK
signal signCompleted(string topic, string id, bool userAccepted, string error)
function requestReceived(event, dappName, dappUrl, dappIcon, connectorId) {
d.onSessionRequestEvent(event, dappName, dappUrl, dappIcon, connectorId)
}
function requestResolved(topic, id) {
root.requests.removeRequest(topic, id)
}
function requestExpired(sessionId) {
d.onSessionRequestExpired(sessionId)
}
onRejected: (topic, id, hasError) => {
root.sdk.rejectSessionRequest(topic, id, hasError)
}
onAccepted: (topic, id, data) => {
root.sdk.acceptSessionRequest(topic, id, data)
}
Component {
id: sessionRequestComponent
SessionRequestWithAuth {
id: request
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) {
root.accepted(topic, id, data)
} else {
request.reject(true)
}
}
onActiveChanged: {
if (active === false) {
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
if (active === true) {
d.subscribeForFeeUpdates(request.topic, request.requestId)
}
}
onRejected: (hasError) => {
root.rejected(request.topic, request.requestId, hasError)
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
onAuthFailed: () => {
root.rejected(request.topic, request.requestId, true /*hasError*/)
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
onExecute: (password, pin) => {
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
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) {
root.rejected(request.topic, request.requestId, true /*hasError*/)
root.store.signingResult.disconnect(request.signedHandler)
}
}
}
}
Connections {
target: root.sdk
function onSessionRequestEvent(sessionRequest) {
const { id, topic } = sessionRequest
const dapp = SQUtils.ModelUtils.getFirstModelEntryIf(root.dappsModel, (dapp) => {
if (dapp.topic === topic) {
return true
}
return !!SQUtils.ModelUtils.getFirstModelEntryIf(dapp.rawSessions, (session) => {
if (session.topic === topic) {
return true
}
})
})
if (!dapp) {
console.warn("Error finding dapp for topic", topic, "id", id)
root.sdk.rejectSessionRequest(topic, id, true)
return
}
root.requestReceived(sessionRequest, dapp.name, dapp.url, dapp.iconUrl, dapp.connectorId)
}
function onSessionRequestExpired(sessionId) {
root.requestExpired(sessionId)
}
function onSessionRequestUserAnswerResult(topic, id, accept, error) {
let request = root.requests.findRequest(topic, id)
if (request === null) {
console.error("Error finding event for topic", topic, "id", id)
return
}
Qt.callLater(() => root.requestResolved(topic, id))
if (error) {
root.signCompleted(topic, id, accept, error)
console.error(`Error accepting session request for topic: ${topic}, id: ${id}, accept: ${accept}, error: ${error}`)
return
}
root.signCompleted(topic, id, accept, "")
}
}
QtObject {
id: d
function onSessionRequestEvent(event, dappName, dappUrl, dappIcon, connectorId) {
try {
const res = d.resolve(event, dappName, dappUrl, dappIcon, connectorId)
if (res.conde === SessionRequest.Ignored) {
return
}
if (res.code !== SessionRequest.NoError) {
root.rejected(event.topic, event.id, true)
return
}
root.requests.enqueue(res.obj)
} catch (e) {
console.error("Error processing session request event", e)
root.rejected(event.topic, event.id, true)
}
}
function onSessionRequestExpired(sessionId) {
// Expired event coming from WC
// Handling as a failsafe in case the event is not processed by the SDK
let request = root.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()
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
// returns {
// obj: obj or nil
// code: SessionRequest.ErrorCode
// }
function resolve(event, dappName, dappUrl, dappIcon, connectorId) {
const {request, error} = SessionRequestResolver.resolveEvent(event, root.accountsModel, root.networksModel, root.store.hexToDec)
if (error !== SessionRequest.NoError) {
return { obj: null, code: error }
}
if (!request) {
return { obj: null, code: SessionRequest.RuntimeError }
}
const mainNet = lookupMainnetNetwork()
if (!mainNet) {
console.error("Mainnet network not found")
return { obj: null, code: SessionRequest.RuntimeError }
}
updateFeesOnPreparedData(request)
let obj = sessionRequestComponent.createObject(null, {
event: request.event,
topic: request.topic,
requestId: request.requestId,
method: request.method,
accountAddress: request.account,
chainId: request.chainId,
data: request.data,
preparedData: JSON.stringify(request.preparedData),
expirationTimestamp: request.expiryTimestamp,
dappName,
dappUrl,
dappIcon,
sourceId: connectorId,
value: request.value
})
if (obj === null) {
console.error("Error creating SessionRequestResolved for event")
return { obj: null, code: SessionRequest.RuntimeError }
}
if (!request.transaction) {
obj.haveEnoughFunds = true
return { obj: obj, code: SessionRequest.NoError }
}
updateFeesParamsToPassedObj(obj)
return {
obj: obj,
code: SessionRequest.NoError
}
}
// Updates the fees to a SessionRequestResolved
function updateFeesParamsToPassedObj(requestItem) {
if (!(requestItem instanceof SessionRequestResolved)) {
return
}
if (!SessionRequest.isTransactionMethod(requestItem.method)) {
return
}
const mainNet = lookupMainnetNetwork()
if (!mainNet) {
console.error("Mainnet network not found")
return { obj: null, code: SessionRequest.RuntimeError }
}
const tx = SessionRequest.getTxObject(requestItem.method, requestItem.data)
requestItem.estimatedTimeCategory = root.store.getEstimatedTime(requestItem.chainId, tx.maxFeePerGas || tx.gasPrice || "")
let st = getEstimatedFeesStatus(tx, requestItem.method, requestItem.chainId, mainNet.chainId)
let fundsStatus = checkFundsStatus(st.feesInfo.maxFees, st.feesInfo.l1GasFee, requestItem.accountAddress, requestItem.chainId, mainNet.chainId, requestItem.value)
requestItem.fiatMaxFees = st.fiatMaxFees
requestItem.ethMaxFees = st.maxFeesEth
requestItem.haveEnoughFunds = fundsStatus.haveEnoughFunds
requestItem.haveEnoughFees = fundsStatus.haveEnoughForFees
requestItem.feesInfo = st.feesInfo
}
// Updates the fee in the transaction preview on a JS Object built by SessionRequest
function updateFeesOnPreparedData(request) {
if (!request.transaction && !request.preparedData instanceof Object) {
return
}
let fees = root.store.getSuggestedFees(request.chainId)
if (!request.preparedData.maxFeePerGas
&& request.preparedData.hasOwnProperty("maxFeePerGas")
&& fees.eip1559Enabled) {
request.preparedData.maxFeePerGas = d.getFeesForFeesMode(fees)
}
if (!request.preparedData.maxPriorityFeePerGas
&& request.preparedData.hasOwnProperty("maxPriorityFeePerGas")
&& fees.eip1559Enabled) {
request.preparedData.maxPriorityFeePerGas = fees.maxPriorityFeePerGas
}
if (!request.preparedData.gasPrice
&& request.preparedData.hasOwnProperty("gasPrice")
&& !fees.eip1559Enabled) {
request.preparedData.gasPrice = fees.gasPrice
}
}
/// Returns null if the network is not found
function lookupMainnetNetwork() {
return SQUtils.ModelUtils.getByKey(root.networksModel, "layer", 1)
}
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) {
root.store.signMessageUnsafe(request.topic,
request.requestId,
request.accountAddress,
SessionRequest.methods.personalSign.getMessageFromData(request.data),
password,
pin)
} else if (request.method === SessionRequest.methods.personalSign.name) {
root.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
root.store.safeSignTypedData(request.topic,
request.requestId,
request.accountAddress,
SessionRequest.methods.signTypedData.getMessageFromData(request.data),
request.chainId,
legacy,
password,
pin)
} else if (SessionRequest.isTransactionMethod(request.method)) {
let txObj = SessionRequest.getTxObject(request.method, request.data)
if (!!payload) {
let hexFeesJson = root.store.convertFeesInfoToHex(JSON.stringify(payload))
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) {
root.store.signTransaction(request.topic,
request.requestId,
request.accountAddress,
request.chainId,
txObj,
password,
pin)
} else if (request.method === SessionRequest.methods.sendTransaction.name) {
root.store.sendTransaction(
request.topic,
request.requestId,
request.accountAddress,
request.chainId,
txObj,
password,
pin)
}
}
return true
}
// Returns {
// maxFees -> Big number in Gwei
// maxFeePerGas
// maxPriorityFeePerGas
// gasPrice
// }
function getEstimatedMaxFees(tx, method, chainId, mainNetChainId) {
const BigOps = SQUtils.AmountsArithmetic
const gasLimit = BigOps.fromString("21000")
const parsedTransaction = SessionRequest.parseTransaction(tx, root.store.hexToDec)
let gasPrice = BigOps.fromString(parsedTransaction.maxFeePerGas)
let maxFeePerGas = BigOps.fromString(parsedTransaction.maxFeePerGas)
let maxPriorityFeePerGas = BigOps.fromString(parsedTransaction.maxPriorityFeePerGas)
let l1GasFee = BigOps.fromNumber(0)
if (!maxFeePerGas || !maxPriorityFeePerGas || !gasPrice) {
const suggesteFees = getSuggestedFees(chainId)
maxFeePerGas = suggesteFees.maxFeePerGas
maxPriorityFeePerGas = suggesteFees.maxPriorityFeePerGas
gasPrice = suggesteFees.gasPrice
l1GasFee = suggesteFees.l1GasFee
}
let maxFees = BigOps.times(gasLimit, gasPrice)
return {maxFees, maxFeePerGas, maxPriorityFeePerGas, gasPrice, l1GasFee}
}
function getSuggestedFees(chainId) {
const BigOps = SQUtils.AmountsArithmetic
const fees = root.store.getSuggestedFees(chainId)
const maxPriorityFeePerGas = fees.maxPriorityFeePerGas
let maxFeePerGas
let gasPrice
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
}
}
const l1GasFee = BigOps.fromNumber(fees.l1GasFee)
return {maxFeePerGas, maxPriorityFeePerGas, gasPrice, l1GasFee}
}
// Returned values are Big numbers
function getEstimatedFeesStatus(tx, method, chainId, mainNetChainId) {
const BigOps = SQUtils.AmountsArithmetic
const feesInfo = getEstimatedMaxFees(tx, method, chainId, mainNetChainId)
const totalMaxFees = BigOps.sum(feesInfo.maxFees, feesInfo.l1GasFee)
const maxFeesEth = BigOps.div(totalMaxFees, BigOps.fromNumber(1, 9))
const maxFeesEthStr = maxFeesEth.toString()
const fiatMaxFeesStr = root.getFiatValue(maxFeesEthStr, Constants.ethToken)
const fiatMaxFees = BigOps.fromString(fiatMaxFeesStr)
const symbol = root.currentCurrency
return {fiatMaxFees, maxFeesEth, symbol, feesInfo}
}
function getBalanceInEth(balances, address, chainId) {
const BigOps = SQUtils.AmountsArithmetic
const 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
}
const 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, value) {
const BigOps = SQUtils.AmountsArithmetic
let valueEth = BigOps.fromString(value)
let haveEnoughForFees = true
let haveEnoughFunds = true
let token = SQUtils.ModelUtils.getByKey(root.groupedAccountAssetsModel, "tokensKey", Constants.ethToken)
if (!token || !token.balances) {
console.error("Error token balances lookup for ETH", SQUtils.ModelUtils.modelToArray(root.groupedAccountAssetsModel))
console.error("Looking for tokensKey: ", Constants.ethToken)
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}
}
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()
}
}
}
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)
}
}
}
}
}
}