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:
parent
f60c3321ce
commit
f42f342731
|
@ -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 =
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue