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

410 lines
16 KiB
QML
Raw Normal View History

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
import "../internal"
/// 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
// SessionRequestsModel where the requests are stored
// This component will append and remove requests from this model
required property SessionRequestsModel requests
// The fees broker that provides the updated fees
property TransactionFeesBroker feesBroker: TransactionFeesBroker {
id: feesBroker
store: root.store
}
// 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) {
const request = root.requests.findRequest(topic, id)
if (!request) {
console.error("Error finding request for topic", topic, "id", id)
return
}
root.requests.removeRequest(topic, id)
request.destroy()
}
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
estimatedTimeCategory: feesSubscriber.estimatedTimeResponse
feesInfo: feesSubscriber.feesInfo
haveEnoughFunds: d.hasEnoughEth(request.chainId, request.accountAddress, request.value)
haveEnoughFees: haveEnoughFunds && d.hasEnoughEth(request.chainId, request.accountAddress, request.ethMaxFees)
ethMaxFees: feesSubscriber.maxEthFee ? SQUtils.AmountsArithmetic.div(feesSubscriber.maxEthFee, SQUtils.AmountsArithmetic.fromNumber(1, 9)) : null
fiatMaxFees: ethMaxFees ? SQUtils.AmountsArithmetic.fromString(root.getFiatValue(ethMaxFees.toString(), Constants.ethToken)) : null
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) {
feesBroker.subscribe(feesSubscriber)
}
}
onAccepted: {
active = false
}
onExpired: {
active = false
}
onRejected: (hasError) => {
active = false
root.rejected(request.topic, request.requestId, hasError)
}
onAuthFailed: () => {
root.rejected(request.topic, request.requestId, true /*hasError*/)
}
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) {
root.rejected(request.topic, request.requestId, true /*hasError*/)
root.store.signingResult.disconnect(request.signedHandler)
}
active = false
}
TransactionFeesSubscriber {
id: feesSubscriber
key: request.requestId
chainId: request.chainId
txObject: SessionRequest.getTxObject(request.method, request.data)
active: request.active && !!txObject
selectedFeesMode: Constants.FeesMode.Medium
hexToDec: root.store.hexToDec
}
}
}
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)
const action = accept ? "accepting" : "rejecting"
console.error(`Error ${action} 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, e.stack)
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()
}
// 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 }
}
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 }
}
return {
obj: obj,
code: SessionRequest.NoError
}
}
function hasEnoughEth(chainId, accountAddress, requiredEth) {
if (!requiredEth) {
return true
}
if (!accountAddress || !chainId) {
console.error("No account or chain provided to check funds", accountAddress, chainId)
return true
}
const token = SQUtils.ModelUtils.getByKey(root.groupedAccountAssetsModel, "tokensKey", Constants.ethToken)
const balance = getBalance(chainId, accountAddress, token)
if (!balance) {
console.error("Error fetching balance for account", accountAddress, "on chain", chainId)
return true
}
const BigOps = SQUtils.AmountsArithmetic
const haveEnoughFunds = BigOps.cmp(balance, requiredEth) >= 0
return haveEnoughFunds
}
function getBalance(chainId, address, token) {
if (!token || !token.balances) {
console.error("Error token balances lookup", token)
return null
}
const BigOps = SQUtils.AmountsArithmetic
const accEth = SQUtils.ModelUtils.getFirstModelEntryIf(token.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))
}
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)) {
const txObj = prepareTxForStatusGo(SessionRequest.getTxObject(request.method, request.data), payload)
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
}
function prepareTxForStatusGo(txObj, feesInfo) {
if (!!feesInfo) {
let hexFeesJson = root.store.convertFeesInfoToHex(JSON.stringify(feesInfo))
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
delete txObj.gas
delete txObj.type
}
// Remove nonce from txObj to be auto-filled by the wallet
delete txObj.nonce
return txObj
}
}
}