Alex Jbanca 793aeb15c3 fix(Dapps): Fixing fees in transaction requests
Fixes:
1. Fixing the laggy scrolling on transaction requiests popups. The root cause of this issue was the fees request and also the estimated time request. These periodic requests were blocking. Now we'll call these API async.
2. Fixing the max fees: The fees computation was using 21k as gasLimit. This value was hardcoded in WC. Now we're requesting the gasLimit if it's not provided by the dApp. This call is also async.
3. Fixing the periodicity of the fees computation. The fees were computed by the client only if the tx object didn't already provide the fees. But the tx could fail if when the fees are highly volatile because it was not being overridden. Now Status is computing the fees periodically for all tx requests.
4. Fixing an issue where the loading state of the fees text in the modal was showing text underneath the loading animation. Fixed by updating the AnimatedText to support a custom target property. The text component used for session requests is using `cusomColor` property to set the text color and the `color` for the text must not be overriden.
2024-11-22 11:32:41 +02:00

410 lines
16 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
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
}
}
}