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.
This commit is contained in:
Alex Jbanca 2024-11-18 15:20:10 +02:00
parent f60c3321ce
commit f42f342731
No known key found for this signature in database
GPG Key ID: 6004079575C21C5D
18 changed files with 777 additions and 462 deletions

View File

@ -176,7 +176,7 @@ proc newModule*(
result.filter = initFilter(result.controller)
result.walletConnectService = wc_service.newService(result.events, result.threadpool, settingsService, transactionService, keycardService)
result.walletConnectController = wc_controller.newController(result.walletConnectService, walletAccountService)
result.walletConnectController = wc_controller.newController(result.walletConnectService, walletAccountService, result.events)
result.dappsConnectorService = connector_service.newService(result.events)
result.dappsConnectorController = connector_controller.newController(result.dappsConnectorService, result.events)
@ -360,6 +360,7 @@ method load*(self: Module) =
self.sendModule.load()
self.networksModule.load()
self.walletConnectService.init()
self.walletConnectController.init()
self.dappsConnectorService.init()
method isLoaded*(self: Module): bool =

View File

@ -1,6 +1,7 @@
import NimQml
import chronicles, times, json
import app/core/eventemitter
import app/global/global_singleton
import app_service/common/utils
import app_service/service/wallet_connect/service as wallet_connect_service
@ -19,18 +20,40 @@ QtObject:
Controller* = ref object of QObject
service: wallet_connect_service.Service
walletAccountService: wallet_account_service.Service
events: EventEmitter
proc delete*(self: Controller) =
self.QObject.delete
proc newController*(service: wallet_connect_service.Service, walletAccountService: wallet_account_service.Service): Controller =
proc newController*(
service: wallet_connect_service.Service,
walletAccountService: wallet_account_service.Service,
events: EventEmitter): Controller =
new(result, delete)
result.service = service
result.walletAccountService = walletAccountService
result.events = events
result.QObject.setup
proc estimatedTimeResponse*(self: Controller, topic: string, estimatedTime: int) {.signal.}
proc suggestedFeesResponse*(self: Controller, topic: string, suggestedFeesJson: string) {.signal.}
proc estimatedGasResponse*(self: Controller, topic: string, gasEstimate: string) {.signal.}
proc init*(self: Controller) =
self.events.on(SIGNAL_ESTIMATED_TIME_RESPONSE) do(e: Args):
let args = EstimatedTimeArgs(e)
self.estimatedTimeResponse(args.topic, args.estimatedTime)
self.events.on(SIGNAL_SUGGESTED_FEES_RESPONSE) do(e: Args):
let args = SuggestedFeesArgs(e)
self.suggestedFeesResponse(args.topic, $(args.suggestedFees))
self.events.on(SIGNAL_ESTIMATED_GAS_RESPONSE) do(e: Args):
let args = EstimatedGasArgs(e)
self.estimatedGasResponse(args.topic, args.estimatedGas)
## signals emitted by this controller
proc userAuthenticationResult*(self: Controller, topic: string, id: string, error: bool, password: string, pin: string, payload: string) {.signal.}
proc signingResultReceived*(self: Controller, topic: string, id: string, data: string) {.signal.}
@ -195,12 +218,15 @@ QtObject:
error "sendTransaction failed: ", msg=e.msg
self.signingResultReceived(topic, id, res)
proc getEstimatedTime(self: Controller, chainId: int, maxFeePerGasHex: string): int {.slot.} =
return self.service.getEstimatedTime(chainId, maxFeePerGasHex).int
proc requestEstimatedTime(self: Controller, topic: string, chainId: int, maxFeePerGasHex: string) {.slot.} =
self.service.getEstimatedTime(topic, chainId, maxFeePerGasHex)
proc getSuggestedFeesJson(self: Controller, chainId: int): string {.slot.} =
let dto = self.service.getSuggestedFees(chainId)
return dto.toJson()
proc requestSuggestedFeesJson(self: Controller, topic: string, chainId: int) {.slot.} =
self.service.requestSuggestedFees(topic, chainId)
proc requestGasEstimate(self: Controller, topic: string, chainId: int, txJson: string) {.slot.} =
let txObj = parseJson(txJson)
self.service.requestGasEstimate(topic, txObj, chainId)
proc hexToDecBigString*(self: Controller, hex: string): string {.slot.} =
try:

View File

@ -0,0 +1,91 @@
import backend/backend
import backend/eth
type
AsyncGetEstimatedTimeArgs = ref object of QObjectTaskArg
topic: string
chainId: int
maxFeePerGasHex: string
AsyncSuggestedFeesArgs = ref object of QObjectTaskArg
topic: string
chainId: int
AsyncEstimateGasArgs = ref object of QObjectTaskArg
topic: string
chainId: int
txJson: string
proc asyncGetEstimatedTimeTask(argsEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncGetEstimatedTimeArgs](argsEncoded)
let result = %*{
"topic": arg.topic,
"chainId": arg.chainId,
"estimatedTime": EstimatedTime.Unknown,
}
try:
var maxFeePerGas: float64
if arg.maxFeePerGasHex.isEmptyOrWhitespace:
let chainFeesResult = eth.suggestedFees(arg.chainId).result
let chainFees = chainFeesResult.toSuggestedFeesDto()
if chainFees.isNil:
arg.finish(result)
# For non-EIP-1559 chains, we use the high fee
if chainFees.eip1559Enabled:
maxFeePerGas = chainFees.maxFeePerGasM
else:
maxFeePerGas = chainFees.maxFeePerGasL
else:
try:
let maxFeePerGasInt = parseHexInt(arg.maxFeePerGasHex)
maxFeePerGas = maxFeePerGasInt.float
except ValueError:
error "failed to parse maxFeePerGasHex", msg = arg.maxFeePerGasHex
arg.finish(result)
let estimatedTime = backend.getTransactionEstimatedTime(arg.chainId, $(maxFeePerGas)).result.getInt
result["estimatedTime"] = %estimatedTime
arg.finish(result)
except Exception as e:
error "asyncGetEstimatedTime failed: ", msg=e.msg
arg.finish(result)
proc asyncSuggestedFeesTask(argsEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncSuggestedFeesArgs](argsEncoded)
let result = %*{
"topic": arg.topic,
"chainId": arg.chainId,
"suggestedFees": %*{},
}
try:
let response = eth.suggestedFees(arg.chainId)
result["suggestedFees"] = response.result
arg.finish(result)
except Exception as e:
error "asyncSuggestedFees failed: ", msg=e.msg
arg.finish(result)
proc asyncEstimateGasTask(argsEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncEstimateGasArgs](argsEncoded)
let result = %*{
"topic": arg.topic,
"chainId": arg.chainId,
"estimatedGas": "",
}
try:
let tx = parseJson(arg.txJson)
let transaction = %*{
"from": tx["from"].getStr,
"to": tx["to"].getStr,
"data": tx["data"].getStr
}
if tx.hasKey("value"):
transaction["value"] = tx["value"]
let response = eth.estimateGas(arg.chainId, %* [transaction])
result["estimatedGas"] = response.result
arg.finish(result)
except Exception as e:
error "asyncGasLimit failed: ", msg=e.msg
arg.finish(result)

View File

@ -14,21 +14,45 @@ import app/global/global_singleton
import app/core/eventemitter
import app/core/signals/types
import app/core/tasks/[threadpool]
import app/core/[main]
import app/core/tasks/[qt, threadpool]
import app/modules/shared_modules/keycard_popup/io_interface as keycard_shared_module
include app_service/common/json_utils
include app/core/tasks/common
include async_tasks
logScope:
topics = "wallet-connect-service"
# include async_tasks
const UNIQUE_WALLET_CONNECT_MODULE_IDENTIFIER* = "WalletSection-WCModule"
const SIGNAL_ESTIMATED_TIME_RESPONSE* = "estimatedTimeResponse"
const SIGNAL_SUGGESTED_FEES_RESPONSE* = "suggestedFeesResponse"
const SIGNAL_ESTIMATED_GAS_RESPONSE* = "estimatedGasResponse"
type
AuthenticationResponseFn* = proc(keyUid: string, password: string, pin: string)
SignResponseFn* = proc(keyUid: string, signature: string)
type
EstimatedTimeArgs* = ref object of Args
topic*: string
chainId*: int
estimatedTime*: int
SuggestedFeesArgs* = ref object of Args
topic*: string
chainId*: int
suggestedFees*: JsonNode
EstimatedGasArgs* = ref object of Args
topic*: string
chainId*: int
estimatedGas*: string
QtObject:
type Service* = ref object of QObject
events: EventEmitter
@ -206,64 +230,107 @@ QtObject:
return response.getStr
# empty maxFeePerGasHex will fetch the current chain's maxFeePerGas
proc getEstimatedTime*(self: Service, chainId: int, maxFeePerGasHex: string): EstimatedTime =
var maxFeePerGas: float64
if maxFeePerGasHex.isEmptyOrWhitespace:
let chainFees = self.transactions.suggestedFees(chainId)
if chainFees.isNil:
return EstimatedTime.Unknown
proc getEstimatedTime*(self: Service, topic: string, chainId: int, maxFeePerGasHex: string) =
let request = AsyncGetEstimatedTimeArgs(
tptr: asyncGetEstimatedTimeTask,
vptr: cast[ByteAddress](self.vptr),
slot: "estimatedTimeResponse",
topic: topic,
chainId: chainId,
maxFeePerGasHex: maxFeePerGasHex
)
self.threadpool.start(request)
# For non-EIP-1559 chains, we use the high fee
if chainFees.eip1559Enabled:
maxFeePerGas = chainFees.maxFeePerGasM
else:
maxFeePerGas = chainFees.maxFeePerGasL
else:
try:
let maxFeePerGasInt = parseHexInt(maxFeePerGasHex)
maxFeePerGas = maxFeePerGasInt.float
except ValueError:
error "failed to parse maxFeePerGasHex", maxFeePerGasHex
return EstimatedTime.Unknown
proc estimatedTimeResponse*(self: Service, response: string) {.slot.} =
try:
let responseObj = response.parseJson
let args = EstimatedTimeArgs(
topic: responseObj["topic"].getStr,
chainId: responseObj["chainId"].getInt,
estimatedTime: responseObj["estimatedTime"].getInt
)
self.events.emit(SIGNAL_ESTIMATED_TIME_RESPONSE, args)
except Exception as e:
error "failed to parse estimated time response", msg = e.msg
return self.transactions.getEstimatedTime(chainId, $(maxFeePerGas))
proc requestSuggestedFees*(self: Service, topic: string, chainId: int) =
let request = AsyncSuggestedFeesArgs(
tptr: asyncSuggestedFeesTask,
vptr: cast[ByteAddress](self.vptr),
slot: "suggestedFeesResponse",
topic: topic,
chainId: chainId
)
self.threadpool.start(request)
proc getSuggestedFees*(self: Service, chainId: int): SuggestedFeesDto =
return self.transactions.suggestedFees(chainId)
proc suggestedFeesResponse*(self: Service, response: string) {.slot.} =
try:
let responseObj = response.parseJson
let args = SuggestedFeesArgs(
topic: responseObj["topic"].getStr,
chainId: responseObj["chainId"].getInt,
suggestedFees: responseObj["suggestedFees"]
)
self.events.emit(SIGNAL_SUGGESTED_FEES_RESPONSE, args)
except Exception as e:
error "failed to parse suggested fees response", msg = e.msg
proc disconnectKeycardReponseSignal(self: Service) =
self.events.disconnect(self.connectionKeycardResponse)
proc disconnectKeycardReponseSignal(self: Service) =
self.events.disconnect(self.connectionKeycardResponse)
proc connectKeycardReponseSignal(self: Service) =
self.connectionKeycardResponse = self.events.onWithUUID(SIGNAL_KEYCARD_RESPONSE) do(e: Args):
let args = KeycardLibArgs(e)
self.disconnectKeycardReponseSignal()
if self.signCallback == nil:
error "unexpected user authenticated event; no callback set"
return
defer:
self.signCallback = nil
let currentFlow = self.keycardService.getCurrentFlow()
if currentFlow != KCSFlowType.Sign:
error "unexpected keycard flow type: ", currentFlow
self.signCallback("", "")
return
let signature = "0x" &
singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.r) &
singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.s) &
singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.v)
self.signCallback(args.flowEvent.keyUid, signature)
proc connectKeycardReponseSignal(self: Service) =
self.connectionKeycardResponse = self.events.onWithUUID(SIGNAL_KEYCARD_RESPONSE) do(e: Args):
let args = KeycardLibArgs(e)
self.disconnectKeycardReponseSignal()
if self.signCallback == nil:
error "unexpected user authenticated event; no callback set"
return
defer:
self.signCallback = nil
let currentFlow = self.keycardService.getCurrentFlow()
if currentFlow != KCSFlowType.Sign:
error "unexpected keycard flow type: ", currentFlow
self.signCallback("", "")
return
let signature = "0x" &
singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.r) &
singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.s) &
singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.v)
self.signCallback(args.flowEvent.keyUid, signature)
proc cancelCurrentFlow*(self: Service) =
self.keycardService.cancelCurrentFlow()
proc cancelCurrentFlow*(self: Service) =
self.keycardService.cancelCurrentFlow()
proc runSigningOnKeycard*(self: Service, keyUid: string, path: string, hashedMessageToSign: string, pin: string, callback: SignResponseFn): bool =
if pin.len == 0:
return false
if self.signCallback != nil:
return false
self.signCallback = callback
self.cancelCurrentFlow()
self.connectKeycardReponseSignal()
self.keycardService.startSignFlow(path, hashedMessageToSign, pin)
return true
proc runSigningOnKeycard*(self: Service, keyUid: string, path: string, hashedMessageToSign: string, pin: string, callback: SignResponseFn): bool =
if pin.len == 0:
return false
if self.signCallback != nil:
return false
self.signCallback = callback
self.cancelCurrentFlow()
self.connectKeycardReponseSignal()
self.keycardService.startSignFlow(path, hashedMessageToSign, pin)
return true
proc requestGasEstimate*(self: Service, topic: string, tx: JsonNode, chainId: int) =
let request = AsyncEstimateGasArgs(
tptr: asyncEstimateGasTask,
vptr: cast[ByteAddress](self.vptr),
slot: "estimatedGasResponse",
topic: topic,
chainId: chainId,
txJson: $tx
)
self.threadpool.start(request)
proc estimatedGasResponse*(self: Service, response: string) {.slot.} =
try:
let responseObj = response.parseJson
let args = EstimatedGasArgs(
topic: responseObj["topic"].getStr,
chainId: responseObj["chainId"].getInt,
estimatedGas: responseObj["estimatedGas"].getStr
)
self.events.emit(SIGNAL_ESTIMATED_GAS_RESPONSE, args)
except Exception as e:
error "failed to parse estimated gas response", msg = e.msg

View File

@ -44,9 +44,10 @@ SplitView {
cryptoFees: "0.001"
estimatedTime: "3-5 minutes"
feesLoading: feesLoading.checked
estimatedTimeLoading: feesLoading.checked
hasFees: hasFees.checked
enoughFundsForTransaction: enoughFeesForTransaction.checked
enoughFundsForFees: enoughFeesForGas.checked
enoughFundsForFees: enoughFeesForGas.checked || !feesLoading.checked
// sun emoji
accountEmoji: "\u2600"
@ -133,7 +134,7 @@ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Fusce nibh. Etiam quis
CheckBox {
id: feesLoading
text: "Fees loading"
checked: false
checked: true
}
CheckBox {
id: hasFees

View File

@ -78,8 +78,11 @@ Item {
walletConnectEnabled: wcService.walletConnectFeatureEnabled
connectorEnabled: wcService.connectorFeatureEnabled
//formatBigNumber: (number, symbol, noSymbolOption) => wcService.walletRootStore.currencyStore.formatBigNumber(number, symbol, noSymbolOption)
formatBigNumber: (number, symbol, noSymbolOption) => {
print ("formatBigNumber", number, symbol, noSymbolOption)
return parseFloat(number).toLocaleString(Qt.locale(), 'f', 2)
+ (noSymbolOption ? "" : " " + (symbol || Qt.locale().currencySymbol(Locale.CurrencyIsoCode)))
}
onDisconnectRequested: (connectionId) => wcService.disconnectDapp(connectionId)
onPairingRequested: (uri) => wcService.pair(uri)
onPairingValidationRequested: (uri) => wcService.validatePairingUri(uri)
@ -87,6 +90,7 @@ Item {
onConnectionDeclined: (pairingId) => wcService.rejectPairSession(pairingId)
onSignRequestAccepted: (connectionId, requestId) => wcService.sign(connectionId, requestId)
onSignRequestRejected: (connectionId, requestId) => wcService.rejectSign(connectionId, requestId, false /*hasError*/)
onSignRequestIsLive: (connectionId, requestId) => wcService.signRequestIsLive(connectionId, requestId)
Connections {
target: dappsWorkflow.wcService
@ -170,7 +174,7 @@ Item {
model: dappsService.sessionRequestsModel
delegate: RowLayout {
StatusBaseText {
text: SQUtils.Utils.elideAndFormatWalletAddress(model.topic, 6, 4)
text: SQUtils.Utils.elideAndFormatWalletAddress(model.requestItem.topic, 6, 4)
Layout.fillWidth: true
}
}
@ -393,6 +397,10 @@ Item {
signal userAuthenticationFailed(string topic, string id)
signal signingResult(string topic, string id, string data)
signal activeSessionsReceived(var activeSessionsJsonObj, bool success)
// Fees and gas
signal estimatedTimeResponse(string topic, int timeCategory, bool success)
signal suggestedFeesResponse(string topic, var suggestedFeesJsonObj, bool success)
signal estimatedGasResponse(string topic, string gasEstimate, bool success)
function addWalletConnectSession(sessionJson) {
console.info("Add Persisted Session", sessionJson)
@ -479,8 +487,17 @@ Item {
signingResult(topic, id, "0xf8672a8402fb7acf82520894e2d622c817878da5143bbe068")
}
function getEstimatedTime(chainId, maxFeePerGas) {
return Constants.TransactionEstimatedTime.LessThanThreeMins
function requestEstimatedTime(topic, chainId, maxFeePerGasHex) {
estimatedTimeResponse(topic, Constants.TransactionEstimatedTime.LessThanThreeMins, true)
}
function requestSuggestedFees(topic, chainId) {
const suggestedFees = getSuggestedFees()
suggestedFeesResponse(topic, suggestedFees, true)
}
function requestGasEstimate(topic, chainId, txObj) {
estimatedGasResponse(topic, "0x5208", true)
}
function getSuggestedFees() {
@ -488,9 +505,9 @@ Item {
gasPrice: 2.0,
baseFee: 5.0,
maxPriorityFeePerGas: 2.0,
maxFeePerGasL: 1.0,
maxFeePerGasM: 1.1,
maxFeePerGasH: 1.2,
maxFeePerGasLow: 1.0,
maxFeePerGasMedium: 1.1,
maxFeePerGasHigh: 1.2,
l1GasFee: 4.0,
eip1559Enabled: true
}
@ -569,7 +586,8 @@ Item {
sessions.forEach(function(session) {
sessionsModel.append(session)
let firstIconUrl = session.peer.metadata.icons.length > 0 ? session.peer.metadata.icons[0] : ""
let firstIconUrl = !!session.peer.metadata.icons && session.peer.metadata.icons.length > 0 ?
session.peer.metadata.icons[0] : ""
let persistedDapp = {
"name": session.peer.metadata.name,
"url": session.peer.metadata.url,

View File

@ -150,6 +150,11 @@ Item {
signal userAuthenticationFailed(string topic, string id)
signal signingResult(string topic, string id, string data)
signal estimatedTimeResponse(string topic, int timeCategory, bool success)
signal suggestedFeesResponse(string topic, var suggestedFeesJsonObj, bool success)
signal estimatedGasResponse(string topic, string gasEstimate, bool success)
// By default, return no dapps in store
function getDapps() {
dappsListReceived(dappsListReceivedJsonStr)
@ -181,23 +186,27 @@ Item {
updateWalletConnectSessionsCalls.push({activeTopicsJson})
}
function getEstimatedTime(chainId, maxFeePerGas) {
return Constants.TransactionEstimatedTime.LessThanThreeMins
function requestEstimatedTime(topic, chainId, maxFeePerGas) {
estimatedTimeResponse(topic, Constants.TransactionEstimatedTime.LessThanThreeMins, true)
}
property var mockedSuggestedFees: ({
gasPrice: 2.0,
baseFee: 5.0,
maxPriorityFeePerGas: 2.0,
maxFeePerGasL: 1.0,
maxFeePerGasM: 1.1,
maxFeePerGasH: 1.2,
maxFeePerGasLow: 1.0,
maxFeePerGasMedium: 1.1,
maxFeePerGasHigh: 1.2,
l1GasFee: 0.0,
eip1559Enabled: true
})
function getSuggestedFees() {
return mockedSuggestedFees
function requestSuggestedFees(topic, chainId) {
suggestedFeesResponse(topic, mockedSuggestedFees, true)
}
function requestGasEstimate(topic, chainId, tx) {
estimatedGasResponse(topic, "0x5208", true)
}
function hexToDec(hex) {
@ -447,7 +456,7 @@ Item {
// Override the suggestedFees
if (!!data.maxFeePerGasM) {
handler.store.mockedSuggestedFees.maxFeePerGasM = data.maxFeePerGasM
handler.store.mockedSuggestedFees.maxFeePerGasMedium = data.maxFeePerGasM
}
if (!!data.l1GasFee) {
handler.store.mockedSuggestedFees.l1GasFee = data.l1GasFee
@ -473,10 +482,11 @@ Item {
callback({"b536a": JSON.parse(Testing.formatApproveSessionResponse([chainId, 7], [testAddress]))})
let request = handler.requestsModel.findById(session.id)
request.setActive()
verify(!!request, "expected request to be found")
compare(request.fiatMaxFees.toFixed(), data.expect.fee, "expected ethMaxFees to be set")
compare(request.fiatMaxFees.toFixed(), data.expect.fee, "expected fiatMaxFees to be set")
// storybook's CurrenciesStore mock up getFiatValue returns the balance
compare(request.ethMaxFees, data.expect.fee, "expected fiatMaxFees to be set")
compare(request.ethMaxFees, data.expect.fee, "expected ethMaxFees to be set")
verify(request.haveEnoughFunds, "expected haveEnoughFunds to be set")
compare(request.haveEnoughFees, data.expect.haveEnoughForFees, "expected haveEnoughForFees to be set")
verify(!!request.feesInfo, "expected feesInfo to be set")

View File

@ -47,6 +47,10 @@ Item {
signal userAuthenticationFailed(string topic, string id)
signal signingResult(string topic, string id, string data)
signal estimatedTimeResponse(string topic, int timeCategory, bool success)
signal suggestedFeesResponse(string topic, var suggestedFeesJsonObj, bool success)
signal estimatedGasResponse(string topic, string gasEstimate, bool success)
function hexToDec(hex) {
return parseInt(hex, 16)
}
@ -122,7 +126,6 @@ Item {
address: "0x123"
}
}
currentCurrency: "USD"
requests: SessionRequestsModel {}
getFiatValue: (balance, cryptoSymbol) => {
return parseFloat(balance)

View File

@ -323,6 +323,7 @@ DappsComboBox {
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

View File

@ -10,6 +10,8 @@ import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0
import utils 1.0
import "./internal"
/// Component that provides the dapps integration for the wallet.
/// It provides the following features:
/// - WalletConnect integration
@ -263,6 +265,12 @@ SQUtils.QObject {
}
}
// The fees broker to handle all fees requests for all components and connections
TransactionFeesBroker {
id: feesBroker
store: root.store
}
// bcSignRequestPlugin and wcSignRequestPlugin are used to handle sign requests
// Almost identical, and it's worth extracting in an inline component, but Qt5.15.2 doesn't support it
SignRequestPlugin {
@ -272,10 +280,10 @@ SQUtils.QObject {
groupedAccountAssetsModel: root.groupedAccountAssetsModel
networksModel: root.networksModel
accountsModel: root.accountsModel
currentCurrency: root.currenciesStore.currentCurrency
store: root.store
requests: root.requestsModel
dappsModel: root.dappsModel
feesBroker: feesBroker
getFiatValue: (value, currency) => {
return root.currenciesStore.getFiatValue(value, currency)
@ -293,10 +301,10 @@ SQUtils.QObject {
groupedAccountAssetsModel: root.groupedAccountAssetsModel
networksModel: root.networksModel
accountsModel: root.accountsModel
currentCurrency: root.currenciesStore.currentCurrency
store: root.store
requests: root.requestsModel
dappsModel: root.dappsModel
feesBroker: feesBroker
getFiatValue: (value, currency) => {
return root.currenciesStore.getFiatValue(value, currency)

View File

@ -0,0 +1,155 @@
import QtQuick 2.15
import AppLayouts.Wallet.services.dapps.types 1.0
import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0
// The TransactionFeesBroker is responsible for managing the subscriptions to the estimated time, fees and gas limit for a transaction
// It will bundle the same requests to optimise the backend load and will notify the subscribers when the data is ready
// It can only work with a TransactionFeesSubscriber
SQUtils.QObject {
id: root
required property DAppsStore store
property int interval: 5000
function subscribe(subscriberObj) {
if (!(subscriberObj instanceof TransactionFeesSubscriber)) {
console.error("Invalid subscriber object")
return
}
try {
const estimatedTimeSub = estimatedTimeSubscription.createObject(subscriberObj, {
subscriber: subscriberObj
})
const feesSub = feesSubscription.createObject(subscriberObj, {
subscriber: subscriberObj
})
const gasLimitSub = gasLimitSubscription.createObject(subscriberObj, {
subscriber: subscriberObj
})
if (subscriberObj.txObject && (subscriberObj.txObject.gas || subscriberObj.txObject.gasLimit)) {
subscriberObj.setGas(subscriberObj.txObject.gas || subscriberObj.txObject.gasLimit)
}
broker.subscribe(estimatedTimeSub)
broker.subscribe(feesSub)
broker.subscribe(gasLimitSub)
} catch (e) {
console.error("Error subscribing to estimated time: ", e)
}
}
enum SubscriptionType {
Fees,
GasLimit,
EstimatedTime
}
SQUtils.SubscriptionBroker {
id: broker
active: Qt.application.state === Qt.ApplicationActive
onRequest: d.computeFees(topic)
}
SQUtils.QObject {
id: d
function computeFees(topic) {
try {
const args = JSON.parse(topic)
switch (args.type) {
case TransactionFeesBroker.SubscriptionType.Fees:
root.store.requestSuggestedFees(topic, args.chainId)
break
case TransactionFeesBroker.SubscriptionType.GasLimit:
root.store.requestGasEstimate(topic, args.chainId, args.tx)
break
case TransactionFeesBroker.SubscriptionType.EstimatedTime:
root.store.requestEstimatedTime(topic, args.chainId, args.maxFeePerGasHex)
break
}
} catch (e) {
console.error("Error computing fees: ", e)
}
}
}
Component {
id: feesSubscription
SQUtils.Subscription {
required property TransactionFeesSubscriber subscriber
readonly property var requestArgs: ({
type: TransactionFeesBroker.SubscriptionType.Fees,
chainId: subscriber.chainId
})
isReady: subscriber.active
topic: isReady ? JSON.stringify(requestArgs) : ""
onResponseChanged: {
if (!response || !response.success)
return
subscriber.setFees(response.suggestedFees)
}
}
}
Component {
id: estimatedTimeSubscription
SQUtils.Subscription {
required property TransactionFeesSubscriber subscriber
readonly property var requestArgs: ({
type: TransactionFeesBroker.SubscriptionType.EstimatedTime,
chainId: subscriber.chainId,
maxFeePerGasHex: subscriber.txObject ? (subscriber.txObject.maxFeePerGas || subscriber.txObject.gasPrice || "") :
""
})
isReady: subscriber.active
topic: isReady ? JSON.stringify(requestArgs) : ""
onResponseChanged: {
if (!response || !response.success)
return
subscriber.setEstimatedTime(response.estimatedTime)
}
notificationInterval: root.interval
}
}
Component {
id: gasLimitSubscription
SQUtils.Subscription {
required property TransactionFeesSubscriber subscriber
readonly property var requestArgs: ({
type: TransactionFeesBroker.SubscriptionType.GasLimit,
chainId: subscriber.chainId,
tx: subscriber.txObject
})
isReady: subscriber.active && !!subscriber.txObject && !!subscriber.chainId && !subscriber.gasLimit /*Ask for gas just once*/
topic: isReady ? JSON.stringify(requestArgs) : ""
onResponseChanged: {
if (!response || !response.success)
return
subscriber.setGas(response.gasEstimate)
}
notificationInterval: root.interval
}
}
Connections {
id: storeConnections
target: root.store
function onEstimatedTimeResponse(topic, timeCategory, success) {
broker.response(topic, { estimatedTime: timeCategory, success})
}
function onSuggestedFeesResponse(topic, suggestedFeesJson, success) {
broker.response(topic, { suggestedFees: suggestedFeesJson, success: success })
}
function onEstimatedGasResponse(topic, gasEstimate, success) {
broker.response(topic, { gasEstimate, success })
}
}
}

View File

@ -0,0 +1,134 @@
import QtQuick 2.15
import StatusQ.Core.Utils 0.1 as SQUtils
import utils 1.0
SQUtils.QObject {
id: root
/*
Input properties
*/
// standard transaction object
required property var txObject
// subscriber id
required property string key
// chainId -> chain id for the transaction
required property int chainId
// active specifies if the subscriber has an active subscription
required property bool active
// selectedFeesMode -> selected fees mode. Defaults to Constants.TransactionFeesMode.Medium
property int selectedFeesMode: Constants.TransactionFeesMode.Medium
// Required function to be implemented by the subscriber
required property var hexToDec /*(hexValue) => decValue*/
/*
Published properties
*/
// estimatedTimeResponse -> maps to Constants.TransactionEstimatedTime
readonly property int estimatedTimeResponse: d.estimatedTimeResponse
// maxEthFee -> Big number in Gwei. Represens the total fees for the transaction
readonly property var maxEthFee: d.computedFees
// feesInfo -> status-go fees info with updated maxFeePerGas based on selectedFeesMode
readonly property var feesInfo: d.computedFeesInfo
// gasLimit -> gas limit for the transaction
readonly property var gasLimit: d.gasResponse
function setFees(fees) {
if (d.feesResponse === fees) {
return
}
d.feesResponse = fees
d.resetFees()
}
function setGas(gas) {
if (d.gasResponse === gas) {
return
}
d.gasResponse = gas
d.resetFees()
}
function setEstimatedTime(estimatedTime) {
if(!estimatedTime) {
estimatedTime = Constants.TransactionEstimatedTime.Unknown
return
}
d.estimatedTimeResponse = estimatedTime
}
QtObject {
id: d
property int estimatedTimeResponse: Constants.TransactionEstimatedTime.Unknown
property var feesResponse
property var gasResponse
// Eth max fee in Gwei
property var computedFees
// feesResponse with additional `maxFeePerGas` property based on selectedFeesMode
property var computedFeesInfo
function resetFees() {
if (!d.feesResponse) {
return
}
if (!gasResponse) {
return
}
try {
d.computedFees = getEstimatedMaxFees()
d.computedFeesInfo = d.feesResponse
d.computedFeesInfo.maxFeePerGas = getFeesForFeesMode(d.feesResponse)
} catch (e) {
console.error("Failed to compute fees", e, e.stack)
}
}
function getFeesForFeesMode(feesObj) {
if (!(feesObj.hasOwnProperty("maxFeePerGasLow") &&
feesObj.hasOwnProperty("maxFeePerGasMedium") &&
feesObj.hasOwnProperty("maxFeePerGasHigh"))) {
print ("feesObj", JSON.stringify(feesObj))
throw new Error("inappropriate fees object provided")
}
const BigOps = SQUtils.AmountsArithmetic
if (!feesObj.eip1559Enabled && !!feesObj.gasPrice) {
return feesObj.gasPrice
}
switch (root.selectedFeesMode) {
case Constants.FeesMode.Low:
return feesObj.maxFeePerGasLow
case Constants.FeesMode.Medium:
return feesObj.maxFeePerGasMedium
case Constants.FeesMode.High:
return feesObj.maxFeePerGasHigh
default:
throw new Error("unknown selected mode")
}
}
function getEstimatedMaxFees() {
// Note: Use the received fee arguments only once!
// Complete what's missing with the suggested fees
const BigOps = SQUtils.AmountsArithmetic
const gasLimitStr = root.hexToDec(d.gasResponse)
const gasLimit = BigOps.fromString(gasLimitStr)
const maxFeesPerGas = BigOps.fromNumber(getFeesForFeesMode(d.feesResponse))
const l1GasFee = d.feesResponse.l1GasFee ? BigOps.fromNumber(d.feesResponse.l1GasFee)
: BigOps.fromNumber(0)
let maxGasFee = BigOps.times(gasLimit, maxFeesPerGas).plus(l1GasFee)
return maxGasFee
}
Component.onCompleted: {
resetFees()
}
}
}

View File

@ -10,6 +10,8 @@ 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
@ -34,11 +36,14 @@ SQUtils.QObject {
/// 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
// 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")
@ -56,7 +61,13 @@ SQUtils.QObject {
}
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) {
@ -77,7 +88,12 @@ SQUtils.QObject {
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
@ -93,26 +109,29 @@ SQUtils.QObject {
}
onActiveChanged: {
if (active === false) {
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
if (active === true) {
d.subscribeForFeeUpdates(request.topic, request.requestId)
if (active) {
feesBroker.subscribe(feesSubscriber)
}
}
onAccepted: {
active = false
}
onExpired: {
active = false
}
onRejected: (hasError) => {
active = false
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 {
@ -125,6 +144,17 @@ SQUtils.QObject {
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
}
}
}
@ -192,7 +222,7 @@ SQUtils.QObject {
}
root.requests.enqueue(res.obj)
} catch (e) {
console.error("Error processing session request event", e)
console.error("Error processing session request event", e, e.stack)
root.rejected(event.topic, event.id, true)
}
}
@ -211,7 +241,6 @@ SQUtils.QObject {
}
request.setExpired()
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
// returns {
// obj: obj or nil
@ -225,13 +254,6 @@ SQUtils.QObject {
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,
@ -254,76 +276,50 @@ SQUtils.QObject {
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
function hasEnoughEth(chainId, accountAddress, requiredEth) {
if (!requiredEth) {
return true
}
if (!SessionRequest.isTransactionMethod(requestItem.method)) {
return
if (!accountAddress || !chainId) {
console.error("No account or chain provided to check funds", accountAddress, chainId)
return true
}
const mainNet = lookupMainnetNetwork()
if (!mainNet) {
console.error("Mainnet network not found")
return { obj: null, code: SessionRequest.RuntimeError }
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 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
const BigOps = SQUtils.AmountsArithmetic
const haveEnoughFunds = BigOps.cmp(balance, requiredEth) >= 0
return haveEnoughFunds
}
// 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
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
}
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)
const accountFundsWei = BigOps.fromString(accEth.balance)
return BigOps.div(accountFundsWei, BigOps.fromNumber(1, 18))
}
function executeSessionRequest(request, password, pin, payload) {
@ -364,24 +360,7 @@ SQUtils.QObject {
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
const txObj = prepareTxForStatusGo(SessionRequest.getTxObject(request.method, request.data), payload)
if (request.method === SessionRequest.methods.signTransaction.name) {
root.store.signTransaction(request.topic,
request.requestId,
@ -405,267 +384,26 @@ SQUtils.QObject {
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}
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
}
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)
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
}
}
}

View File

@ -296,12 +296,12 @@ QtObject {
}
case methods.signTypedData_v4.name: {
const stringPayload = methods.signTypedData_v4.getMessageFromData(data)
payload = JSON.stringify(JSON.parse(stringPayload), null, 2)
payload = JSON.parse(stringPayload)
break
}
case methods.signTypedData.name: {
const stringPayload = methods.signTypedData.getMessageFromData(data)
payload = JSON.stringify(JSON.parse(stringPayload), null, 2)
payload = JSON.parse(stringPayload)
break
}
case methods.signTransaction.name:
@ -332,23 +332,35 @@ QtObject {
function parseTransaction(tx, hexToDec) {
let parsedTransaction = Object.assign({}, tx)
if (parsedTransaction.hasOwnProperty("value")) {
parsedTransaction.value = hexToEth(parsedTransaction.value, hexToDec).toString()
}
if (parsedTransaction.hasOwnProperty("maxFeePerGas")) {
parsedTransaction.maxFeePerGas = hexToGwei(parsedTransaction.maxFeePerGas, hexToDec).toString()
}
if (parsedTransaction.hasOwnProperty("maxPriorityFeePerGas")) {
parsedTransaction.maxPriorityFeePerGas = hexToGwei(parsedTransaction.maxPriorityFeePerGas, hexToDec).toString()
}
if (parsedTransaction.hasOwnProperty("gasPrice")) {
parsedTransaction.gasPrice = hexToGwei(parsedTransaction.gasPrice, hexToDec)
parsedTransaction.value = hexToEth(parsedTransaction.value, hexToDec)
}
if (parsedTransaction.hasOwnProperty("gasLimit")) {
parsedTransaction.gasLimit = parseInt(hexToDec(parsedTransaction.gasLimit))
}
if (parsedTransaction.hasOwnProperty("gas")) {
parsedTransaction.gas = parseInt(hexToDec(parsedTransaction.gas))
}
if (parsedTransaction.hasOwnProperty("nonce")) {
parsedTransaction.nonce = parseInt(hexToDec(parsedTransaction.nonce))
}
if (parsedTransaction.hasOwnProperty("from")) {
parsedTransaction.from = parsedTransaction.from
}
if (parsedTransaction.hasOwnProperty("to")) {
parsedTransaction.to = parsedTransaction.to
}
if (parsedTransaction.hasOwnProperty("data")) {
parsedTransaction.data = parsedTransaction.data
}
if (parsedTransaction.hasOwnProperty("maxFeePerGas")) {
delete parsedTransaction.maxFeePerGas
}
if (parsedTransaction.hasOwnProperty("maxPriorityFeePerGas")) {
delete parsedTransaction.maxPriorityFeePerGas
}
if (parsedTransaction.hasOwnProperty("gasPrice")) {
delete parsedTransaction.gasPrice
}
return parsedTransaction
}

View File

@ -47,6 +47,7 @@ QObject {
/// maps to Constants.TransactionEstimatedTime values
property int estimatedTimeCategory: 0
signal expired()
function isExpired() {
return !!expirationTimestamp && expirationTimestamp > 0 && Math.floor(Date.now() / 1000) >= expirationTimestamp
@ -54,6 +55,7 @@ QObject {
function setExpired() {
expirationTimestamp = Math.floor(Date.now() / 1000)
expired()
}
function setActive() {

View File

@ -6,6 +6,7 @@ SequentialAnimation {
id: root
property var target: null
property string targetProperty: "color"
property color fromColor: Theme.palette.directColor1
property color toColor: Theme.palette.getColor(fromColor, 0.1)
property int duration: 500 // in milliseconds
@ -14,7 +15,7 @@ SequentialAnimation {
ColorAnimation {
target: root.target
property: "color"
property: root.targetProperty
from: root.fromColor
to: root.toColor
duration: root.duration
@ -22,7 +23,7 @@ SequentialAnimation {
ColorAnimation {
target: root.target
property: "color"
property: root.targetProperty
from: root.toColor
to: root.fromColor
duration: root.duration

View File

@ -38,6 +38,7 @@ SignTransactionModalBase {
required property string cryptoFees
required property string estimatedTime
required property bool hasFees
required property bool estimatedTimeLoading
property bool enoughFundsForTransaction: true
property bool enoughFundsForFees: false
@ -98,15 +99,17 @@ SignTransactionModalBase {
objectName: "footerFiatFeesText"
text: formatBigNumber(root.fiatFees, root.currentCurrency)
loading: root.feesLoading && root.hasFees
customColor: !root.hasFees || root.enoughFundsForFees ? Theme.palette.directColor1 : Theme.palette.dangerColor1
elide: Qt.ElideMiddle
Binding on text {
when: !root.hasFees
value: qsTr("No fees")
}
Binding on customColor {
value: !root.hasFees || root.enoughFundsForFees ? Theme.palette.directColor1 : Theme.palette.dangerColor1
}
onTextChanged: {
if (text === "") {
if (text === "" || loading) {
return
}
maxFeesAnimation.restart()
@ -115,6 +118,8 @@ SignTransactionModalBase {
AnimatedText {
id: maxFeesAnimation
target: maxFees
targetProperty: "customColor"
running: !maxFees.loading && root.hasFees
fromColor: maxFees.customColor
}
}
@ -131,10 +136,10 @@ SignTransactionModalBase {
id: estimatedTime
objectName: "footerEstimatedTime"
text: root.estimatedTime
loading: root.feesLoading
loading: root.estimatedTimeLoading
onTextChanged: {
if (text === "") {
if (text === "" || loading) {
return
}
estimatedTimeAnimation.restart()
@ -143,6 +148,8 @@ SignTransactionModalBase {
AnimatedText {
id: estimatedTimeAnimation
target: estimatedTime
targetProperty: "customColor"
running: !estimatedTime.loading
}
}
}
@ -203,10 +210,12 @@ SignTransactionModalBase {
horizontalAlignment: Text.AlignRight
font.pixelSize: Theme.additionalTextSize
loading: root.feesLoading
customColor: root.enoughFundsForFees ? Theme.palette.directColor1 : Theme.palette.dangerColor1
Binding on customColor {
value: root.enoughFundsForFees ? Theme.palette.directColor1 : Theme.palette.dangerColor1
}
onTextChanged: {
if (text === "") {
if (text === "" || loading) {
return
}
fiatFeesAnimation.restart()
@ -215,6 +224,7 @@ SignTransactionModalBase {
AnimatedText {
id: fiatFeesAnimation
target: fiatFees
targetProperty: "customColor"
fromColor: fiatFees.customColor
}
}
@ -225,11 +235,13 @@ SignTransactionModalBase {
text: formatBigNumber(root.cryptoFees, Constants.ethToken)
horizontalAlignment: Text.AlignRight
font.pixelSize: Theme.additionalTextSize
customColor: root.enoughFundsForFees ? Theme.palette.baseColor1 : Theme.palette.dangerColor1
loading: root.feesLoading
Binding on customColor {
value: root.enoughFundsForFees ? Theme.palette.baseColor1 : Theme.palette.dangerColor1
}
onTextChanged: {
if (text === "") {
if (text === "" || loading) {
return
}
cryptoFeesAnimation.restart()
@ -239,6 +251,8 @@ SignTransactionModalBase {
id: cryptoFeesAnimation
target: cryptoFees
fromColor: cryptoFees.customColor
targetProperty: "customColor"
running: !maxFees.loading && root.hasFees
}
}
}

View File

@ -14,6 +14,10 @@ QObject {
signal signingResult(string topic, string id, string data)
signal estimatedTimeResponse(string topic, int timeCategory, bool success)
signal suggestedFeesResponse(string topic, var suggestedFeesJsonObj, bool success)
signal estimatedGasResponse(string topic, string gasEstimate, bool success)
function addWalletConnectSession(sessionJson) {
return controller.addWalletConnectSession(sessionJson)
}
@ -80,14 +84,24 @@ QObject {
// Empty maxFeePerGas will fetch the current chain's maxFeePerGas
// Returns ui/imports/utils -> Constants.TransactionEstimatedTime values
function getEstimatedTime(chainId, maxFeePerGasHex) {
return controller.getEstimatedTime(chainId, maxFeePerGasHex)
function requestEstimatedTime(topic, chainId, maxFeePerGasHex) {
controller.requestEstimatedTime(topic, chainId, maxFeePerGasHex)
}
// Returns nim's SuggestedFeesDto; see src/app_service/service/transaction/dto.nim
// Returns all value initialized to 0 if error
function getSuggestedFees(chainId) {
return JSON.parse(controller.getSuggestedFeesJson(chainId))
function requestSuggestedFees(topic, chainId) {
controller.requestSuggestedFeesJson(topic, chainId)
}
function requestGasEstimate(topic, chainId, txObj) {
try {
let tx = prepareTxForStatusGo(txObj)
controller.requestGasEstimate(topic, chainId, JSON.stringify(tx))
} catch (e) {
console.error("Failed to prepare tx for status-go", e)
root.estimatedGasResponse(topic, "", false)
}
}
function signTransaction(topic, id, address, chainId, password, txObj) {
@ -150,5 +164,24 @@ QObject {
function onSigningResultReceived(topic, id, data) {
root.signingResult(topic, id, data)
}
function onEstimatedTimeResponse(topic, timeCategory) {
root.estimatedTimeResponse(topic, timeCategory, !!timeCategory)
}
function onSuggestedFeesResponse(topic, suggestedFeesJson) {
try {
const jsonObj = JSON.parse(suggestedFeesJson)
root.suggestedFeesResponse(topic, jsonObj, true)
} catch (e) {
console.error("Failed to parse suggestedFeesJson", e)
root.suggestedFeesResponse(topic, {}, false)
return
}
}
function onEstimatedGasResponse(topic, gasEstimate) {
root.estimatedGasResponse(topic, gasEstimate, !!gasEstimate)
}
}
}