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

398 lines
14 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import SortFilterProxyModel 0.2
import AppLayouts.Wallet 1.0
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import shared.popups.walletconnect 1.0
import utils 1.0
DappsComboBox {
id: root
// Values mapped to Constants.LoginType
required property int loginType
/*
Accounts model
Expected model structure:
name [string] - account name e.g. "Piggy Bank"
address [string] - wallet account address e.g. "0x1234567890"
colorizedChainPrefixes [string] - chain prefixes with rich text colors e.g. "<font color=\"red\">eth:</font><font color=\"blue\">oeth:</font><font color=\"green\">arb:</font>"
emoji [string] - emoji for account e.g. "🐷"
colorId [string] - color id for account e.g. "1"
currencyBalance [var] - fiat currency balance
amount [number] - amount of currency e.g. 1234
symbol [string] - currency symbol e.g. "USD"
optDisplayDecimals [number] - optional number of decimals to display
stripTrailingZeroes [bool] - strip trailing zeroes
walletType [string] - wallet type e.g. Constants.watchWalletType. See `Constants` for possible values
migratedToKeycard [bool] - whether account is migrated to keycard
accountBalance [var] - account balance for a specific network
formattedBalance [string] - formatted balance e.g. "1234.56B"
balance [string] - balance e.g. "123456000000"
iconUrl [string] - icon url e.g. "network/Network=Hermez"
chainColor [string] - chain color e.g. "#FF0000"
*/
property var accountsModel
/*
Networks model
Expected model structure:
chainName [string] - chain long name. e.g. "Ethereum" or "Optimism"
chainId [int] - chain unique identifier
iconUrl [string] - SVG icon name. e.g. "network/Network=Ethereum"
layer [int] - chain layer. e.g. 1 or 2
isTest [bool] - true if the chain is a testnet
*/
property var networksModel
/*
ObjectModel containig session requests
requestId [string] - unique identifier for the request
requestItem [SessionRequestResolved] - request object
*/
property SessionRequestsModel sessionRequestsModel
property string selectedAccountAddress
property var formatBigNumber: (number, symbol, noSymbolOption) => console.error("formatBigNumber not set")
signal pairWCReady()
signal disconnectRequested(string connectionId)
signal pairingRequested(string uri)
signal pairingValidationRequested(string uri)
signal connectionAccepted(string pairingId, var chainIds, string selectedAccount)
signal connectionDeclined(string pairingId)
signal signRequestAccepted(string connectionId, string requestId)
signal signRequestRejected(string connectionId, string requestId)
signal signRequestIsLive(string connectionId, string requestId)
/// Response to pairingValidationRequested
function pairingValidated(validationState) {
if (pairWCLoader.item) {
pairWCLoader.item.pairingValidated(validationState)
}
}
/// Confirmation received on connectionAccepted
function connectionSuccessful(pairingId, newConnectionId) {
connectDappLoader.connectionSuccessful(pairingId, newConnectionId)
}
/// Confirmation received on connectionAccepted
function connectionFailed(pairingId) {
connectDappLoader.connectionFailed(pairingId)
}
/// Request to connect to a dApp
function connectDApp(dappChains, dappUrl, dappName, dappIcon, pairingId) {
connectDappLoader.connect(dappChains, dappUrl, dappName, dappIcon, pairingId)
}
onPairDapp: {
pairWCLoader.active = true
}
onDisconnectDapp: (dappUrl) => {
disconnectdAppDialogLoader.dAppUrl = dappUrl
disconnectdAppDialogLoader.active = true
}
Loader {
id: disconnectdAppDialogLoader
property string dAppUrl
active: false
onLoaded: {
const dApp = SQUtils.ModelUtils.getByKey(root.model, "url", dAppUrl);
if (dApp) {
item.dappName = dApp.name;
item.dappIcon = dApp.iconUrl;
item.dappUrl = disconnectdAppDialogLoader.dAppUrl;
}
item.open();
}
sourceComponent: DAppConfirmDisconnectPopup {
visible: true
onClosed: {
disconnectdAppDialogLoader.active = false
}
onAccepted: {
SQUtils.ModelUtils.forEach(model, (dApp) => {
if (dApp.url === dAppUrl) {
root.disconnectRequested(dApp.topic)
}
})
}
}
}
Loader {
id: pairWCLoader
active: false
onLoaded: {
item.open()
root.pairWCReady()
}
sourceComponent: PairWCModal {
visible: true
onClosed: pairWCLoader.active = false
onPair: (uri) => root.pairingRequested(uri)
onPairUriChanged: (uri) => root.pairingValidationRequested(uri)
}
}
Loader {
id: connectDappLoader
active: false
// Array of chaind ids
property var dappChains
property url dappUrl
property string dappName
property url dappIcon
property var key
property var topic
property var connectionQueue: []
onActiveChanged: {
if (!active && connectionQueue.length > 0) {
connect(connectionQueue[0].dappChains,
connectionQueue[0].dappUrl,
connectionQueue[0].dappName,
connectionQueue[0].dappIcon,
connectionQueue[0].key)
connectionQueue.shift()
}
}
function connect(dappChains, dappUrl, dappName, dappIcon, key) {
if (connectDappLoader.active) {
connectionQueue.push({ dappChains, dappUrl, dappName, dappIcon, key })
return
}
connectDappLoader.dappChains = dappChains
connectDappLoader.dappUrl = dappUrl
connectDappLoader.dappName = dappName
connectDappLoader.dappIcon = dappIcon
connectDappLoader.key = key
if (pairWCLoader.item) {
// Allow user to get the uri valid confirmation
pairWCLoader.item.pairingValidated(Pairing.errors.dappReadyForApproval)
connectDappTimer.start()
} else {
connectDappLoader.active = true
}
}
function connectionSuccessful(key, newTopic) {
if (connectDappLoader.key === key && connectDappLoader.item) {
connectDappLoader.topic = newTopic
connectDappLoader.item.pairSuccessful()
}
}
function connectionFailed(id) {
if (connectDappLoader.key === key && connectDappLoader.item) {
connectDappLoader.item.pairFailed()
}
}
sourceComponent: ConnectDAppModal {
visible: true
onClosed: connectDappLoader.active = false
accounts: root.accountsModel
flatNetworks: SortFilterProxyModel {
sourceModel: root.networksModel
filters: [
FastExpressionFilter {
inverted: true
expression: connectDappLoader.dappChains.indexOf(chainId) === -1
expectedRoles: ["chainId"]
}
]
}
selectedAccountAddress: root.selectedAccountAddress
dAppUrl: connectDappLoader.dappUrl
dAppName: connectDappLoader.dappName
dAppIconUrl: connectDappLoader.dappIcon
connectButtonEnabled: root.enabled
onConnect: {
if (!selectedAccount || !selectedAccount.address) {
console.error("Missing account selection")
return
}
if (!selectedChains || selectedChains.length === 0) {
console.error("Missing chain selection")
return
}
root.connectionAccepted(connectDappLoader.key, selectedChains, selectedAccount.address)
}
onDecline: {
root.connectionDeclined(connectDappLoader.key)
close()
}
onDisconnect: {
root.disconnectRequested(connectDappLoader.topic)
close()
}
}
}
Instantiator {
model: root.sessionRequestsModel
delegate: DAppSignRequestModal {
id: dappRequestModal
objectName: "dappsRequestModal"
required property var model
required property int index
readonly property var request: model.requestItem
readonly property var account: accountEntry.available ? accountEntry.item : {
name: "",
address: "",
emoji: "",
colorId: 0,
migratedToKeycard: false
}
readonly property var network: networkEntry.available ? networkEntry.item : {
chainName: "",
iconUrl: ""
}
property bool requestHandled: false
function rejectRequest() {
// Allow rejecting only once
if (requestHandled) {
return
}
requestHandled = true
root.signRequestRejected(request.topic, request.requestId)
}
parent: root
loginType: account.migratedToKeycard ? Constants.LoginType.Keycard : root.loginType
formatBigNumber: root.formatBigNumber
visible: !!request.dappUrl
dappUrl: request.dappUrl
dappIcon: request.dappIcon
dappName: request.dappName
accountColor: Utils.getColorForId(account.colorId)
accountName: account.name
accountAddress: account.address
accountEmoji: account.emoji
networkName: network.chainName
networkIconPath: Theme.svg(network.iconUrl)
fiatFees: request.fiatMaxFees ? request.fiatMaxFees.toFixed() : ""
cryptoFees: request.ethMaxFees ? request.ethMaxFees.toFixed() : ""
estimatedTime: WalletUtils.getLabelForEstimatedTxTime(request.estimatedTimeCategory)
feesLoading: hasFees && (!fiatFees || !cryptoFees)
estimatedTimeLoading: request.estimatedTimeCategory === Constants.TransactionEstimatedTime.Unknown
hasFees: signingTransaction
enoughFundsForTransaction: request.haveEnoughFunds
enoughFundsForFees: request.haveEnoughFees
signButtonEnabled: ((!hasFees) || enoughFundsForTransaction && enoughFundsForFees) && root.enabled
signingTransaction: !!request.method && (request.method === SessionRequest.methods.signTransaction.name
|| request.method === SessionRequest.methods.sendTransaction.name)
requestPayload: {
try {
const data = JSON.parse(request.preparedData)
delete data.maxFeePerGas
delete data.maxPriorityFeePerGas
delete data.gasPrice
return JSON.stringify(data, null, 2)
} catch(_) {
return request.preparedData
}
}
expirationSeconds: request.expirationTimestamp ? request.expirationTimestamp - requestTimestamp.getTime() / 1000
: 0
hasExpiryDate: !!request.expirationTimestamp
onOpened: {
root.signRequestIsLive(request.topic, request.requestId)
}
onClosed: {
Qt.callLater(rejectRequest)
}
onAccepted: {
requestHandled = true
root.signRequestAccepted(request.topic, request.requestId)
}
onRejected: {
rejectRequest()
}
ModelEntry {
id: accountEntry
sourceModel: root.accountsModel
key: "address"
value: request.accountAddress
}
ModelEntry {
id: networkEntry
sourceModel: root.networksModel
key: "chainId"
value: request.chainId
}
}
}
// Used between transitioning from PairWCModal to ConnectDAppModal
Timer {
id: connectDappTimer
interval: 500
running: false
repeat: false
onTriggered: {
pairWCLoader.item.close()
connectDappLoader.active = true
}
}
}