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.filter = initFilter(result.controller)
result.walletConnectService = wc_service.newService(result.events, result.threadpool, settingsService, transactionService, keycardService) 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.dappsConnectorService = connector_service.newService(result.events)
result.dappsConnectorController = connector_controller.newController(result.dappsConnectorService, result.events) result.dappsConnectorController = connector_controller.newController(result.dappsConnectorService, result.events)
@ -360,6 +360,7 @@ method load*(self: Module) =
self.sendModule.load() self.sendModule.load()
self.networksModule.load() self.networksModule.load()
self.walletConnectService.init() self.walletConnectService.init()
self.walletConnectController.init()
self.dappsConnectorService.init() self.dappsConnectorService.init()
method isLoaded*(self: Module): bool = method isLoaded*(self: Module): bool =

View File

@ -1,6 +1,7 @@
import NimQml import NimQml
import chronicles, times, json import chronicles, times, json
import app/core/eventemitter
import app/global/global_singleton import app/global/global_singleton
import app_service/common/utils import app_service/common/utils
import app_service/service/wallet_connect/service as wallet_connect_service import app_service/service/wallet_connect/service as wallet_connect_service
@ -19,18 +20,40 @@ QtObject:
Controller* = ref object of QObject Controller* = ref object of QObject
service: wallet_connect_service.Service service: wallet_connect_service.Service
walletAccountService: wallet_account_service.Service walletAccountService: wallet_account_service.Service
events: EventEmitter
proc delete*(self: Controller) = proc delete*(self: Controller) =
self.QObject.delete 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) new(result, delete)
result.service = service result.service = service
result.walletAccountService = walletAccountService result.walletAccountService = walletAccountService
result.events = events
result.QObject.setup 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 ## signals emitted by this controller
proc userAuthenticationResult*(self: Controller, topic: string, id: string, error: bool, password: string, pin: string, payload: string) {.signal.} 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.} proc signingResultReceived*(self: Controller, topic: string, id: string, data: string) {.signal.}
@ -195,12 +218,15 @@ QtObject:
error "sendTransaction failed: ", msg=e.msg error "sendTransaction failed: ", msg=e.msg
self.signingResultReceived(topic, id, res) self.signingResultReceived(topic, id, res)
proc getEstimatedTime(self: Controller, chainId: int, maxFeePerGasHex: string): int {.slot.} = proc requestEstimatedTime(self: Controller, topic: string, chainId: int, maxFeePerGasHex: string) {.slot.} =
return self.service.getEstimatedTime(chainId, maxFeePerGasHex).int self.service.getEstimatedTime(topic, chainId, maxFeePerGasHex)
proc getSuggestedFeesJson(self: Controller, chainId: int): string {.slot.} = proc requestSuggestedFeesJson(self: Controller, topic: string, chainId: int) {.slot.} =
let dto = self.service.getSuggestedFees(chainId) self.service.requestSuggestedFees(topic, chainId)
return dto.toJson()
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.} = proc hexToDecBigString*(self: Controller, hex: string): string {.slot.} =
try: 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/eventemitter
import app/core/signals/types 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 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: logScope:
topics = "wallet-connect-service" topics = "wallet-connect-service"
# include async_tasks # include async_tasks
const UNIQUE_WALLET_CONNECT_MODULE_IDENTIFIER* = "WalletSection-WCModule" 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 type
AuthenticationResponseFn* = proc(keyUid: string, password: string, pin: string) AuthenticationResponseFn* = proc(keyUid: string, password: string, pin: string)
SignResponseFn* = proc(keyUid: string, signature: 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: QtObject:
type Service* = ref object of QObject type Service* = ref object of QObject
events: EventEmitter events: EventEmitter
@ -206,64 +230,107 @@ QtObject:
return response.getStr return response.getStr
# empty maxFeePerGasHex will fetch the current chain's maxFeePerGas # empty maxFeePerGasHex will fetch the current chain's maxFeePerGas
proc getEstimatedTime*(self: Service, chainId: int, maxFeePerGasHex: string): EstimatedTime = proc getEstimatedTime*(self: Service, topic: string, chainId: int, maxFeePerGasHex: string) =
var maxFeePerGas: float64 let request = AsyncGetEstimatedTimeArgs(
if maxFeePerGasHex.isEmptyOrWhitespace: tptr: asyncGetEstimatedTimeTask,
let chainFees = self.transactions.suggestedFees(chainId) vptr: cast[ByteAddress](self.vptr),
if chainFees.isNil: slot: "estimatedTimeResponse",
return EstimatedTime.Unknown topic: topic,
chainId: chainId,
maxFeePerGasHex: maxFeePerGasHex
)
self.threadpool.start(request)
# For non-EIP-1559 chains, we use the high fee proc estimatedTimeResponse*(self: Service, response: string) {.slot.} =
if chainFees.eip1559Enabled: try:
maxFeePerGas = chainFees.maxFeePerGasM let responseObj = response.parseJson
else: let args = EstimatedTimeArgs(
maxFeePerGas = chainFees.maxFeePerGasL topic: responseObj["topic"].getStr,
else: chainId: responseObj["chainId"].getInt,
try: estimatedTime: responseObj["estimatedTime"].getInt
let maxFeePerGasInt = parseHexInt(maxFeePerGasHex) )
maxFeePerGas = maxFeePerGasInt.float self.events.emit(SIGNAL_ESTIMATED_TIME_RESPONSE, args)
except ValueError: except Exception as e:
error "failed to parse maxFeePerGasHex", maxFeePerGasHex error "failed to parse estimated time response", msg = e.msg
return EstimatedTime.Unknown
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 = proc suggestedFeesResponse*(self: Service, response: string) {.slot.} =
return self.transactions.suggestedFees(chainId) 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) = proc disconnectKeycardReponseSignal(self: Service) =
self.events.disconnect(self.connectionKeycardResponse) self.events.disconnect(self.connectionKeycardResponse)
proc connectKeycardReponseSignal(self: Service) = proc connectKeycardReponseSignal(self: Service) =
self.connectionKeycardResponse = self.events.onWithUUID(SIGNAL_KEYCARD_RESPONSE) do(e: Args): self.connectionKeycardResponse = self.events.onWithUUID(SIGNAL_KEYCARD_RESPONSE) do(e: Args):
let args = KeycardLibArgs(e) let args = KeycardLibArgs(e)
self.disconnectKeycardReponseSignal() self.disconnectKeycardReponseSignal()
if self.signCallback == nil: if self.signCallback == nil:
error "unexpected user authenticated event; no callback set" error "unexpected user authenticated event; no callback set"
return return
defer: defer:
self.signCallback = nil self.signCallback = nil
let currentFlow = self.keycardService.getCurrentFlow() let currentFlow = self.keycardService.getCurrentFlow()
if currentFlow != KCSFlowType.Sign: if currentFlow != KCSFlowType.Sign:
error "unexpected keycard flow type: ", currentFlow error "unexpected keycard flow type: ", currentFlow
self.signCallback("", "") self.signCallback("", "")
return return
let signature = "0x" & let signature = "0x" &
singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.r) & singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.r) &
singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.s) & singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.s) &
singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.v) singletonInstance.utils.removeHexPrefix(args.flowEvent.txSignature.v)
self.signCallback(args.flowEvent.keyUid, signature) self.signCallback(args.flowEvent.keyUid, signature)
proc cancelCurrentFlow*(self: Service) = proc cancelCurrentFlow*(self: Service) =
self.keycardService.cancelCurrentFlow() self.keycardService.cancelCurrentFlow()
proc runSigningOnKeycard*(self: Service, keyUid: string, path: string, hashedMessageToSign: string, pin: string, callback: SignResponseFn): bool = proc runSigningOnKeycard*(self: Service, keyUid: string, path: string, hashedMessageToSign: string, pin: string, callback: SignResponseFn): bool =
if pin.len == 0: if pin.len == 0:
return false return false
if self.signCallback != nil: if self.signCallback != nil:
return false return false
self.signCallback = callback self.signCallback = callback
self.cancelCurrentFlow() self.cancelCurrentFlow()
self.connectKeycardReponseSignal() self.connectKeycardReponseSignal()
self.keycardService.startSignFlow(path, hashedMessageToSign, pin) self.keycardService.startSignFlow(path, hashedMessageToSign, pin)
return true 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" cryptoFees: "0.001"
estimatedTime: "3-5 minutes" estimatedTime: "3-5 minutes"
feesLoading: feesLoading.checked feesLoading: feesLoading.checked
estimatedTimeLoading: feesLoading.checked
hasFees: hasFees.checked hasFees: hasFees.checked
enoughFundsForTransaction: enoughFeesForTransaction.checked enoughFundsForTransaction: enoughFeesForTransaction.checked
enoughFundsForFees: enoughFeesForGas.checked enoughFundsForFees: enoughFeesForGas.checked || !feesLoading.checked
// sun emoji // sun emoji
accountEmoji: "\u2600" accountEmoji: "\u2600"
@ -133,7 +134,7 @@ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Fusce nibh. Etiam quis
CheckBox { CheckBox {
id: feesLoading id: feesLoading
text: "Fees loading" text: "Fees loading"
checked: false checked: true
} }
CheckBox { CheckBox {
id: hasFees id: hasFees

View File

@ -78,8 +78,11 @@ Item {
walletConnectEnabled: wcService.walletConnectFeatureEnabled walletConnectEnabled: wcService.walletConnectFeatureEnabled
connectorEnabled: wcService.connectorFeatureEnabled 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) onDisconnectRequested: (connectionId) => wcService.disconnectDapp(connectionId)
onPairingRequested: (uri) => wcService.pair(uri) onPairingRequested: (uri) => wcService.pair(uri)
onPairingValidationRequested: (uri) => wcService.validatePairingUri(uri) onPairingValidationRequested: (uri) => wcService.validatePairingUri(uri)
@ -87,6 +90,7 @@ Item {
onConnectionDeclined: (pairingId) => wcService.rejectPairSession(pairingId) onConnectionDeclined: (pairingId) => wcService.rejectPairSession(pairingId)
onSignRequestAccepted: (connectionId, requestId) => wcService.sign(connectionId, requestId) onSignRequestAccepted: (connectionId, requestId) => wcService.sign(connectionId, requestId)
onSignRequestRejected: (connectionId, requestId) => wcService.rejectSign(connectionId, requestId, false /*hasError*/) onSignRequestRejected: (connectionId, requestId) => wcService.rejectSign(connectionId, requestId, false /*hasError*/)
onSignRequestIsLive: (connectionId, requestId) => wcService.signRequestIsLive(connectionId, requestId)
Connections { Connections {
target: dappsWorkflow.wcService target: dappsWorkflow.wcService
@ -170,7 +174,7 @@ Item {
model: dappsService.sessionRequestsModel model: dappsService.sessionRequestsModel
delegate: RowLayout { delegate: RowLayout {
StatusBaseText { StatusBaseText {
text: SQUtils.Utils.elideAndFormatWalletAddress(model.topic, 6, 4) text: SQUtils.Utils.elideAndFormatWalletAddress(model.requestItem.topic, 6, 4)
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
@ -393,6 +397,10 @@ Item {
signal userAuthenticationFailed(string topic, string id) signal userAuthenticationFailed(string topic, string id)
signal signingResult(string topic, string id, string data) signal signingResult(string topic, string id, string data)
signal activeSessionsReceived(var activeSessionsJsonObj, bool success) 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) { function addWalletConnectSession(sessionJson) {
console.info("Add Persisted Session", sessionJson) console.info("Add Persisted Session", sessionJson)
@ -479,8 +487,17 @@ Item {
signingResult(topic, id, "0xf8672a8402fb7acf82520894e2d622c817878da5143bbe068") signingResult(topic, id, "0xf8672a8402fb7acf82520894e2d622c817878da5143bbe068")
} }
function getEstimatedTime(chainId, maxFeePerGas) { function requestEstimatedTime(topic, chainId, maxFeePerGasHex) {
return Constants.TransactionEstimatedTime.LessThanThreeMins 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() { function getSuggestedFees() {
@ -488,9 +505,9 @@ Item {
gasPrice: 2.0, gasPrice: 2.0,
baseFee: 5.0, baseFee: 5.0,
maxPriorityFeePerGas: 2.0, maxPriorityFeePerGas: 2.0,
maxFeePerGasL: 1.0, maxFeePerGasLow: 1.0,
maxFeePerGasM: 1.1, maxFeePerGasMedium: 1.1,
maxFeePerGasH: 1.2, maxFeePerGasHigh: 1.2,
l1GasFee: 4.0, l1GasFee: 4.0,
eip1559Enabled: true eip1559Enabled: true
} }
@ -569,7 +586,8 @@ Item {
sessions.forEach(function(session) { sessions.forEach(function(session) {
sessionsModel.append(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 = { let persistedDapp = {
"name": session.peer.metadata.name, "name": session.peer.metadata.name,
"url": session.peer.metadata.url, "url": session.peer.metadata.url,

View File

@ -150,6 +150,11 @@ Item {
signal userAuthenticationFailed(string topic, string id) signal userAuthenticationFailed(string topic, string id)
signal signingResult(string topic, string id, string data) 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 // By default, return no dapps in store
function getDapps() { function getDapps() {
dappsListReceived(dappsListReceivedJsonStr) dappsListReceived(dappsListReceivedJsonStr)
@ -181,23 +186,27 @@ Item {
updateWalletConnectSessionsCalls.push({activeTopicsJson}) updateWalletConnectSessionsCalls.push({activeTopicsJson})
} }
function getEstimatedTime(chainId, maxFeePerGas) { function requestEstimatedTime(topic, chainId, maxFeePerGas) {
return Constants.TransactionEstimatedTime.LessThanThreeMins estimatedTimeResponse(topic, Constants.TransactionEstimatedTime.LessThanThreeMins, true)
} }
property var mockedSuggestedFees: ({ property var mockedSuggestedFees: ({
gasPrice: 2.0, gasPrice: 2.0,
baseFee: 5.0, baseFee: 5.0,
maxPriorityFeePerGas: 2.0, maxPriorityFeePerGas: 2.0,
maxFeePerGasL: 1.0, maxFeePerGasLow: 1.0,
maxFeePerGasM: 1.1, maxFeePerGasMedium: 1.1,
maxFeePerGasH: 1.2, maxFeePerGasHigh: 1.2,
l1GasFee: 0.0, l1GasFee: 0.0,
eip1559Enabled: true eip1559Enabled: true
}) })
function getSuggestedFees() { function requestSuggestedFees(topic, chainId) {
return mockedSuggestedFees suggestedFeesResponse(topic, mockedSuggestedFees, true)
}
function requestGasEstimate(topic, chainId, tx) {
estimatedGasResponse(topic, "0x5208", true)
} }
function hexToDec(hex) { function hexToDec(hex) {
@ -447,7 +456,7 @@ Item {
// Override the suggestedFees // Override the suggestedFees
if (!!data.maxFeePerGasM) { if (!!data.maxFeePerGasM) {
handler.store.mockedSuggestedFees.maxFeePerGasM = data.maxFeePerGasM handler.store.mockedSuggestedFees.maxFeePerGasMedium = data.maxFeePerGasM
} }
if (!!data.l1GasFee) { if (!!data.l1GasFee) {
handler.store.mockedSuggestedFees.l1GasFee = data.l1GasFee handler.store.mockedSuggestedFees.l1GasFee = data.l1GasFee
@ -473,10 +482,11 @@ Item {
callback({"b536a": JSON.parse(Testing.formatApproveSessionResponse([chainId, 7], [testAddress]))}) callback({"b536a": JSON.parse(Testing.formatApproveSessionResponse([chainId, 7], [testAddress]))})
let request = handler.requestsModel.findById(session.id) let request = handler.requestsModel.findById(session.id)
request.setActive()
verify(!!request, "expected request to be found") 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 // 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") verify(request.haveEnoughFunds, "expected haveEnoughFunds to be set")
compare(request.haveEnoughFees, data.expect.haveEnoughForFees, "expected haveEnoughForFees to be set") compare(request.haveEnoughFees, data.expect.haveEnoughForFees, "expected haveEnoughForFees to be set")
verify(!!request.feesInfo, "expected feesInfo 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 userAuthenticationFailed(string topic, string id)
signal signingResult(string topic, string id, string data) 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) { function hexToDec(hex) {
return parseInt(hex, 16) return parseInt(hex, 16)
} }
@ -122,7 +126,6 @@ Item {
address: "0x123" address: "0x123"
} }
} }
currentCurrency: "USD"
requests: SessionRequestsModel {} requests: SessionRequestsModel {}
getFiatValue: (balance, cryptoSymbol) => { getFiatValue: (balance, cryptoSymbol) => {
return parseFloat(balance) return parseFloat(balance)

View File

@ -323,6 +323,7 @@ DappsComboBox {
cryptoFees: request.ethMaxFees ? request.ethMaxFees.toFixed() : "" cryptoFees: request.ethMaxFees ? request.ethMaxFees.toFixed() : ""
estimatedTime: WalletUtils.getLabelForEstimatedTxTime(request.estimatedTimeCategory) estimatedTime: WalletUtils.getLabelForEstimatedTxTime(request.estimatedTimeCategory)
feesLoading: hasFees && (!fiatFees || !cryptoFees) feesLoading: hasFees && (!fiatFees || !cryptoFees)
estimatedTimeLoading: request.estimatedTimeCategory === Constants.TransactionEstimatedTime.Unknown
hasFees: signingTransaction hasFees: signingTransaction
enoughFundsForTransaction: request.haveEnoughFunds enoughFundsForTransaction: request.haveEnoughFunds
enoughFundsForFees: request.haveEnoughFees enoughFundsForFees: request.haveEnoughFees

View File

@ -10,6 +10,8 @@ import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0 import shared.stores 1.0
import utils 1.0 import utils 1.0
import "./internal"
/// Component that provides the dapps integration for the wallet. /// Component that provides the dapps integration for the wallet.
/// It provides the following features: /// It provides the following features:
/// - WalletConnect integration /// - 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 // 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 // Almost identical, and it's worth extracting in an inline component, but Qt5.15.2 doesn't support it
SignRequestPlugin { SignRequestPlugin {
@ -272,10 +280,10 @@ SQUtils.QObject {
groupedAccountAssetsModel: root.groupedAccountAssetsModel groupedAccountAssetsModel: root.groupedAccountAssetsModel
networksModel: root.networksModel networksModel: root.networksModel
accountsModel: root.accountsModel accountsModel: root.accountsModel
currentCurrency: root.currenciesStore.currentCurrency
store: root.store store: root.store
requests: root.requestsModel requests: root.requestsModel
dappsModel: root.dappsModel dappsModel: root.dappsModel
feesBroker: feesBroker
getFiatValue: (value, currency) => { getFiatValue: (value, currency) => {
return root.currenciesStore.getFiatValue(value, currency) return root.currenciesStore.getFiatValue(value, currency)
@ -293,10 +301,10 @@ SQUtils.QObject {
groupedAccountAssetsModel: root.groupedAccountAssetsModel groupedAccountAssetsModel: root.groupedAccountAssetsModel
networksModel: root.networksModel networksModel: root.networksModel
accountsModel: root.accountsModel accountsModel: root.accountsModel
currentCurrency: root.currenciesStore.currentCurrency
store: root.store store: root.store
requests: root.requestsModel requests: root.requestsModel
dappsModel: root.dappsModel dappsModel: root.dappsModel
feesBroker: feesBroker
getFiatValue: (value, currency) => { getFiatValue: (value, currency) => {
return root.currenciesStore.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 shared.stores 1.0
import utils 1.0 import utils 1.0
import "../internal"
/// Plugin that listens for session requests and manages the lifecycle of the request. /// Plugin that listens for session requests and manages the lifecycle of the request.
SQUtils.QObject { SQUtils.QObject {
id: root id: root
@ -34,11 +36,14 @@ SQUtils.QObject {
/// Expected to have the following roles: /// Expected to have the following roles:
/// - address /// - address
required property var accountsModel required property var accountsModel
/// App currency
required property string currentCurrency
// SessionRequestsModel where the requests are stored // SessionRequestsModel where the requests are stored
// This component will append and remove requests from this model // This component will append and remove requests from this model
required property SessionRequestsModel requests 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 // Function to transform the eth value to fiat
property var getFiatValue: (maxFeesEthStr, token /*Constants.ethToken*/) => console.error("getFiatValue not implemented") property var getFiatValue: (maxFeesEthStr, token /*Constants.ethToken*/) => console.error("getFiatValue not implemented")
@ -56,7 +61,13 @@ SQUtils.QObject {
} }
function requestResolved(topic, id) { 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) root.requests.removeRequest(topic, id)
request.destroy()
} }
function requestExpired(sessionId) { function requestExpired(sessionId) {
@ -77,7 +88,12 @@ SQUtils.QObject {
SessionRequestWithAuth { SessionRequestWithAuth {
id: request id: request
store: root.store 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) { function signedHandler(topic, id, data) {
if (topic != request.topic || id != request.requestId) { if (topic != request.topic || id != request.requestId) {
return return
@ -93,26 +109,29 @@ SQUtils.QObject {
} }
onActiveChanged: { onActiveChanged: {
if (active === false) { if (active) {
d.unsubscribeForFeeUpdates(request.topic, request.requestId) feesBroker.subscribe(feesSubscriber)
}
if (active === true) {
d.subscribeForFeeUpdates(request.topic, request.requestId)
} }
} }
onAccepted: {
active = false
}
onExpired: {
active = false
}
onRejected: (hasError) => { onRejected: (hasError) => {
active = false
root.rejected(request.topic, request.requestId, hasError) root.rejected(request.topic, request.requestId, hasError)
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
} }
onAuthFailed: () => { onAuthFailed: () => {
root.rejected(request.topic, request.requestId, true /*hasError*/) root.rejected(request.topic, request.requestId, true /*hasError*/)
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
} }
onExecute: (password, pin) => { onExecute: (password, pin) => {
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
root.store.signingResult.connect(request.signedHandler) root.store.signingResult.connect(request.signedHandler)
let executed = false let executed = false
try { try {
@ -125,6 +144,17 @@ SQUtils.QObject {
root.rejected(request.topic, request.requestId, true /*hasError*/) root.rejected(request.topic, request.requestId, true /*hasError*/)
root.store.signingResult.disconnect(request.signedHandler) 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) root.requests.enqueue(res.obj)
} catch (e) { } 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) root.rejected(event.topic, event.id, true)
} }
} }
@ -211,7 +241,6 @@ SQUtils.QObject {
} }
request.setExpired() request.setExpired()
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
} }
// returns { // returns {
// obj: obj or nil // obj: obj or nil
@ -225,13 +254,6 @@ SQUtils.QObject {
if (!request) { if (!request) {
return { obj: null, code: SessionRequest.RuntimeError } 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, { let obj = sessionRequestComponent.createObject(null, {
event: request.event, event: request.event,
@ -254,76 +276,50 @@ SQUtils.QObject {
return { obj: null, code: SessionRequest.RuntimeError } return { obj: null, code: SessionRequest.RuntimeError }
} }
if (!request.transaction) {
obj.haveEnoughFunds = true
return { obj: obj, code: SessionRequest.NoError }
}
updateFeesParamsToPassedObj(obj)
return { return {
obj: obj, obj: obj,
code: SessionRequest.NoError code: SessionRequest.NoError
} }
} }
function hasEnoughEth(chainId, accountAddress, requiredEth) {
// Updates the fees to a SessionRequestResolved if (!requiredEth) {
function updateFeesParamsToPassedObj(requestItem) { return true
if (!(requestItem instanceof SessionRequestResolved)) {
return
} }
if (!SessionRequest.isTransactionMethod(requestItem.method)) { if (!accountAddress || !chainId) {
return console.error("No account or chain provided to check funds", accountAddress, chainId)
return true
} }
const mainNet = lookupMainnetNetwork() const token = SQUtils.ModelUtils.getByKey(root.groupedAccountAssetsModel, "tokensKey", Constants.ethToken)
if (!mainNet) { const balance = getBalance(chainId, accountAddress, token)
console.error("Mainnet network not found")
return { obj: null, code: SessionRequest.RuntimeError } if (!balance) {
console.error("Error fetching balance for account", accountAddress, "on chain", chainId)
return true
} }
const tx = SessionRequest.getTxObject(requestItem.method, requestItem.data) const BigOps = SQUtils.AmountsArithmetic
requestItem.estimatedTimeCategory = root.store.getEstimatedTime(requestItem.chainId, tx.maxFeePerGas || tx.gasPrice || "") const haveEnoughFunds = BigOps.cmp(balance, requiredEth) >= 0
return haveEnoughFunds
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 getBalance(chainId, address, token) {
function updateFeesOnPreparedData(request) { if (!token || !token.balances) {
if (!request.transaction && !request.preparedData instanceof Object) { console.error("Error token balances lookup", token)
return 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) const accountFundsWei = BigOps.fromString(accEth.balance)
if (!request.preparedData.maxFeePerGas return BigOps.div(accountFundsWei, BigOps.fromNumber(1, 18))
&& 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) { function executeSessionRequest(request, password, pin, payload) {
@ -364,24 +360,7 @@ SQUtils.QObject {
password, password,
pin) pin)
} else if (SessionRequest.isTransactionMethod(request.method)) { } else if (SessionRequest.isTransactionMethod(request.method)) {
let txObj = SessionRequest.getTxObject(request.method, request.data) const txObj = prepareTxForStatusGo(SessionRequest.getTxObject(request.method, request.data), payload)
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) { if (request.method === SessionRequest.methods.signTransaction.name) {
root.store.signTransaction(request.topic, root.store.signTransaction(request.topic,
request.requestId, request.requestId,
@ -405,267 +384,26 @@ SQUtils.QObject {
return true return true
} }
// Returns { function prepareTxForStatusGo(txObj, feesInfo) {
// maxFees -> Big number in Gwei if (!!feesInfo) {
// maxFeePerGas let hexFeesJson = root.store.convertFeesInfoToHex(JSON.stringify(feesInfo))
// maxPriorityFeePerGas if (!!hexFeesJson) {
// gasPrice let feesInfo = JSON.parse(hexFeesJson)
// } if (feesInfo.maxFeePerGas) {
function getEstimatedMaxFees(tx, method, chainId, mainNetChainId) { txObj.maxFeePerGas = feesInfo.maxFeePerGas
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)) if (feesInfo.maxPriorityFeePerGas) {
const haveEnoughOnMain = BigOps.cmp(mainBalance, feesMain) >= 0 txObj.maxPriorityFeePerGas = feesInfo.maxPriorityFeePerGas
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)
} }
} }
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: { case methods.signTypedData_v4.name: {
const stringPayload = methods.signTypedData_v4.getMessageFromData(data) const stringPayload = methods.signTypedData_v4.getMessageFromData(data)
payload = JSON.stringify(JSON.parse(stringPayload), null, 2) payload = JSON.parse(stringPayload)
break break
} }
case methods.signTypedData.name: { case methods.signTypedData.name: {
const stringPayload = methods.signTypedData.getMessageFromData(data) const stringPayload = methods.signTypedData.getMessageFromData(data)
payload = JSON.stringify(JSON.parse(stringPayload), null, 2) payload = JSON.parse(stringPayload)
break break
} }
case methods.signTransaction.name: case methods.signTransaction.name:
@ -332,23 +332,35 @@ QtObject {
function parseTransaction(tx, hexToDec) { function parseTransaction(tx, hexToDec) {
let parsedTransaction = Object.assign({}, tx) let parsedTransaction = Object.assign({}, tx)
if (parsedTransaction.hasOwnProperty("value")) { if (parsedTransaction.hasOwnProperty("value")) {
parsedTransaction.value = hexToEth(parsedTransaction.value, hexToDec).toString() parsedTransaction.value = hexToEth(parsedTransaction.value, hexToDec)
}
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)
} }
if (parsedTransaction.hasOwnProperty("gasLimit")) { if (parsedTransaction.hasOwnProperty("gasLimit")) {
parsedTransaction.gasLimit = parseInt(hexToDec(parsedTransaction.gasLimit)) parsedTransaction.gasLimit = parseInt(hexToDec(parsedTransaction.gasLimit))
} }
if (parsedTransaction.hasOwnProperty("gas")) {
parsedTransaction.gas = parseInt(hexToDec(parsedTransaction.gas))
}
if (parsedTransaction.hasOwnProperty("nonce")) { if (parsedTransaction.hasOwnProperty("nonce")) {
parsedTransaction.nonce = parseInt(hexToDec(parsedTransaction.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 return parsedTransaction
} }

View File

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

View File

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

View File

@ -38,6 +38,7 @@ SignTransactionModalBase {
required property string cryptoFees required property string cryptoFees
required property string estimatedTime required property string estimatedTime
required property bool hasFees required property bool hasFees
required property bool estimatedTimeLoading
property bool enoughFundsForTransaction: true property bool enoughFundsForTransaction: true
property bool enoughFundsForFees: false property bool enoughFundsForFees: false
@ -98,15 +99,17 @@ SignTransactionModalBase {
objectName: "footerFiatFeesText" objectName: "footerFiatFeesText"
text: formatBigNumber(root.fiatFees, root.currentCurrency) text: formatBigNumber(root.fiatFees, root.currentCurrency)
loading: root.feesLoading && root.hasFees loading: root.feesLoading && root.hasFees
customColor: !root.hasFees || root.enoughFundsForFees ? Theme.palette.directColor1 : Theme.palette.dangerColor1
elide: Qt.ElideMiddle elide: Qt.ElideMiddle
Binding on text { Binding on text {
when: !root.hasFees when: !root.hasFees
value: qsTr("No fees") value: qsTr("No fees")
} }
Binding on customColor {
value: !root.hasFees || root.enoughFundsForFees ? Theme.palette.directColor1 : Theme.palette.dangerColor1
}
onTextChanged: { onTextChanged: {
if (text === "") { if (text === "" || loading) {
return return
} }
maxFeesAnimation.restart() maxFeesAnimation.restart()
@ -115,6 +118,8 @@ SignTransactionModalBase {
AnimatedText { AnimatedText {
id: maxFeesAnimation id: maxFeesAnimation
target: maxFees target: maxFees
targetProperty: "customColor"
running: !maxFees.loading && root.hasFees
fromColor: maxFees.customColor fromColor: maxFees.customColor
} }
} }
@ -131,10 +136,10 @@ SignTransactionModalBase {
id: estimatedTime id: estimatedTime
objectName: "footerEstimatedTime" objectName: "footerEstimatedTime"
text: root.estimatedTime text: root.estimatedTime
loading: root.feesLoading loading: root.estimatedTimeLoading
onTextChanged: { onTextChanged: {
if (text === "") { if (text === "" || loading) {
return return
} }
estimatedTimeAnimation.restart() estimatedTimeAnimation.restart()
@ -143,6 +148,8 @@ SignTransactionModalBase {
AnimatedText { AnimatedText {
id: estimatedTimeAnimation id: estimatedTimeAnimation
target: estimatedTime target: estimatedTime
targetProperty: "customColor"
running: !estimatedTime.loading
} }
} }
} }
@ -203,10 +210,12 @@ SignTransactionModalBase {
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
font.pixelSize: Theme.additionalTextSize font.pixelSize: Theme.additionalTextSize
loading: root.feesLoading 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: { onTextChanged: {
if (text === "") { if (text === "" || loading) {
return return
} }
fiatFeesAnimation.restart() fiatFeesAnimation.restart()
@ -215,6 +224,7 @@ SignTransactionModalBase {
AnimatedText { AnimatedText {
id: fiatFeesAnimation id: fiatFeesAnimation
target: fiatFees target: fiatFees
targetProperty: "customColor"
fromColor: fiatFees.customColor fromColor: fiatFees.customColor
} }
} }
@ -225,11 +235,13 @@ SignTransactionModalBase {
text: formatBigNumber(root.cryptoFees, Constants.ethToken) text: formatBigNumber(root.cryptoFees, Constants.ethToken)
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
font.pixelSize: Theme.additionalTextSize font.pixelSize: Theme.additionalTextSize
customColor: root.enoughFundsForFees ? Theme.palette.baseColor1 : Theme.palette.dangerColor1
loading: root.feesLoading loading: root.feesLoading
Binding on customColor {
value: root.enoughFundsForFees ? Theme.palette.baseColor1 : Theme.palette.dangerColor1
}
onTextChanged: { onTextChanged: {
if (text === "") { if (text === "" || loading) {
return return
} }
cryptoFeesAnimation.restart() cryptoFeesAnimation.restart()
@ -239,6 +251,8 @@ SignTransactionModalBase {
id: cryptoFeesAnimation id: cryptoFeesAnimation
target: cryptoFees target: cryptoFees
fromColor: cryptoFees.customColor 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 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) { function addWalletConnectSession(sessionJson) {
return controller.addWalletConnectSession(sessionJson) return controller.addWalletConnectSession(sessionJson)
} }
@ -80,14 +84,24 @@ QObject {
// Empty maxFeePerGas will fetch the current chain's maxFeePerGas // Empty maxFeePerGas will fetch the current chain's maxFeePerGas
// Returns ui/imports/utils -> Constants.TransactionEstimatedTime values // Returns ui/imports/utils -> Constants.TransactionEstimatedTime values
function getEstimatedTime(chainId, maxFeePerGasHex) { function requestEstimatedTime(topic, chainId, maxFeePerGasHex) {
return controller.getEstimatedTime(chainId, maxFeePerGasHex) controller.requestEstimatedTime(topic, chainId, maxFeePerGasHex)
} }
// Returns nim's SuggestedFeesDto; see src/app_service/service/transaction/dto.nim // Returns nim's SuggestedFeesDto; see src/app_service/service/transaction/dto.nim
// Returns all value initialized to 0 if error // Returns all value initialized to 0 if error
function getSuggestedFees(chainId) { function requestSuggestedFees(topic, chainId) {
return JSON.parse(controller.getSuggestedFeesJson(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) { function signTransaction(topic, id, address, chainId, password, txObj) {
@ -150,5 +164,24 @@ QObject {
function onSigningResultReceived(topic, id, data) { function onSigningResultReceived(topic, id, data) {
root.signingResult(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)
}
} }
} }