diff --git a/src/app/modules/main/wallet_section/transactions/controller.nim b/src/app/modules/main/wallet_section/transactions/controller.nim index 37fd0544b1..742dca126d 100644 --- a/src/app/modules/main/wallet_section/transactions/controller.nim +++ b/src/app/modules/main/wallet_section/transactions/controller.nim @@ -105,4 +105,7 @@ proc getChainIdForChat*(self: Controller): int = return self.networkService.getNetworkForChat().chainId proc getChainIdForBrowser*(self: Controller): int = - return self.networkService.getNetworkForBrowser().chainId \ No newline at end of file + return self.networkService.getNetworkForBrowser().chainId + +proc getEstimatedTime*(self: Controller, priorityFeePerGas: string, maxFeePerGas: string): EstimatedTime = + return self.transactionService.getEstimatedTime(priorityFeePerGas, maxFeePerGas) \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/transactions/io_interface.nim b/src/app/modules/main/wallet_section/transactions/io_interface.nim index d0582231cd..1e4cdcac4f 100644 --- a/src/app/modules/main/wallet_section/transactions/io_interface.nim +++ b/src/app/modules/main/wallet_section/transactions/io_interface.nim @@ -64,6 +64,9 @@ method getChainIdForChat*(self: AccessInterface): int = method getChainIdForBrowser*(self: AccessInterface): int = raise newException(ValueError, "No implementation available") +method getEstimatedTime*(self: AccessInterface, priorityFeePerGas: string, maxFeePerGas: string): int {.base.} = + raise newException(ValueError, "No implementation available") + # View Delegate Interface # Delegate for the view must be declared here due to use of QtObject and multi # inheritance, which is not well supported in Nim. diff --git a/src/app/modules/main/wallet_section/transactions/module.nim b/src/app/modules/main/wallet_section/transactions/module.nim index efad932f43..faffc5f527 100644 --- a/src/app/modules/main/wallet_section/transactions/module.nim +++ b/src/app/modules/main/wallet_section/transactions/module.nim @@ -109,4 +109,7 @@ method getChainIdForChat*(self: Module): int = return self.controller.getChainIdForChat() method getChainIdForBrowser*(self: Module): int = - return self.controller.getChainIdForBrowser() \ No newline at end of file + return self.controller.getChainIdForBrowser() + +method getEstimatedTime*(self: Module, priorityFeePerGas: string, maxFeePerGas: string): int = + return self.controller.getEstimatedTime(priorityFeePerGas, maxFeePerGas).int \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/transactions/view.nim b/src/app/modules/main/wallet_section/transactions/view.nim index a0d302357e..9f40ad8bfd 100644 --- a/src/app/modules/main/wallet_section/transactions/view.nim +++ b/src/app/modules/main/wallet_section/transactions/view.nim @@ -139,4 +139,7 @@ QtObject: return self.delegate.getChainIdForChat() proc getChainIdForBrowser*(self: View): int {.slot.} = - return self.delegate.getChainIdForBrowser() \ No newline at end of file + return self.delegate.getChainIdForBrowser() + + proc getEstimatedTime*(self: View, priorityFeePerGas: string, maxFeePerGas: string): int {.slot.} = + return self.delegate.getEstimatedTime(priorityFeePerGas, maxFeePerGas) \ No newline at end of file diff --git a/src/app_service/service/transaction/service.nim b/src/app_service/service/transaction/service.nim index 6b68edc3aa..1bcf80f38c 100644 --- a/src/app_service/service/transaction/service.nim +++ b/src/app_service/service/transaction/service.nim @@ -33,6 +33,13 @@ include ../../common/json_utils const SIGNAL_TRANSACTIONS_LOADED* = "transactionsLoaded" const SIGNAL_TRANSACTION_SENT* = "transactionSent" +type + EstimatedTime* {.pure.} = enum + Unknown = -1 + LessThanOneMin + LessThanThreeMins + LessThanFiveMins + type TransactionMinedArgs* = ref object of Args data*: string @@ -394,3 +401,69 @@ QtObject: except Exception as e: error "Error fetching crypto services", message = e.msg return @[] + + proc addToAllTransactionsAndSetNewMinMax(self: Service, myTip: float, numOfTransactionWithTipLessThanMine: var int, + transactions: JsonNode) = + if transactions.kind != JArray: + return + for t in transactions: + let gasPriceUnparsed = $fromHex(Stuint[256], t{"gasPrice"}.getStr) + let gasPrice = parseFloat(wei2gwei(gasPriceUnparsed)) + if gasPrice < myTip: + numOfTransactionWithTipLessThanMine.inc + + proc getEstimatedTime*(self: Service, priorityFeePerGas: string, maxFeePerGas: string): EstimatedTime = + let priorityFeePerGasF = priorityFeePerGas.parseFloat + let maxFeePerGasF = maxFeePerGas.parseFloat + var transactionsProcessed = 0 + var numOfTransactionWithTipLessThanMine = 0 + var latestBlockNumber: Option[Uint256] + var expectedBaseFeeForNextBlock: float + try: + let response = eth.getBlockByNumber("latest", true) + if response.error.isNil: + let transactionsJson = response.result{"transactions"} + self.addToAllTransactionsAndSetNewMinMax(priorityFeePerGasF, numOfTransactionWithTipLessThanMine, transactionsJson) + transactionsProcessed = transactionsJson.len + latestBlockNumber = some(stint.fromHex(Uint256, response.result{"number"}.getStr)) + let latestBlockBaseFeePerGasUnparsed = $fromHex(Stuint[256], response.result{"baseFeePerGas"}.getStr) + let latestBlockBaseFeePerGas = parseFloat(wei2gwei(latestBlockBaseFeePerGasUnparsed)) + let latestBlockGasUsedUnparsed = $fromHex(Stuint[256], response.result{"gasUsed"}.getStr) + let latestBlockGasUsed = parseFloat(wei2gwei(latestBlockGasUsedUnparsed)) + let latestBlockGasLimitUnparsed = $fromHex(Stuint[256], response.result{"gasLimit"}.getStr) + let latestBlockGasLimit = parseFloat(wei2gwei(latestBlockGasLimitUnparsed)) + + let ratio = latestBlockGasUsed / latestBlockGasLimit * 0.01 + let maxFeeChange = latestBlockBaseFeePerGas * 0.125 + if(ratio > 50): + expectedBaseFeeForNextBlock = latestBlockBaseFeePerGas + maxFeeChange * (ratio - 50) * 0.01 + else: + expectedBaseFeeForNextBlock = latestBlockBaseFeePerGas - maxFeeChange * (50 - ratio) * 0.01 + except Exception as e: + error "error fetching latest block", msg=e.msg + + if latestBlockNumber.isNone or + priorityFeePerGasF + expectedBaseFeeForNextBlock > maxFeePerGasF: + return EstimatedTime.Unknown + + var blockNumber = latestBlockNumber.get + while (transactionsProcessed < 100 and latestBlockNumber.get < blockNumber + 10): + blockNumber = blockNumber - 1 + try: + let hexPrevNum = "0x" & stint.toHex(blockNumber) + let response = getBlockByNumber(hexPrevNum, true) + let transactionsJson = response.result{"transactions"} + self.addToAllTransactionsAndSetNewMinMax(priorityFeePerGasF, numOfTransactionWithTipLessThanMine, transactionsJson) + transactionsProcessed += transactionsJson.len + except Exception as e: + error "error fetching block number", blockNumber=blockNumber, msg=e.msg + + let p = numOfTransactionWithTipLessThanMine / transactionsProcessed + if p > 0.5: + return EstimatedTime.LessThanOneMin + elif p > 0.35: + return EstimatedTime.LessThanThreeMins + elif p > 0.15: + return EstimatedTime.LessThanFiveMins + else: + return EstimatedTime.Unknown \ No newline at end of file diff --git a/src/backend/eth.nim b/src/backend/eth.nim index 7ae36b1275..d44c4677da 100644 --- a/src/backend/eth.nim +++ b/src/backend/eth.nim @@ -6,8 +6,8 @@ export response_type proc getAccounts*(): RpcResponse[JsonNode] {.raises: [Exception].} = return core.callPrivateRPC("eth_accounts") -proc getBlockByNumber*(blockNumber: string): RpcResponse[JsonNode] {.raises: [Exception].} = - let payload = %* [blockNumber, false] +proc getBlockByNumber*(blockNumber: string, fullTransactionObject = false): RpcResponse[JsonNode] {.raises: [Exception].} = + let payload = %* [blockNumber, fullTransactionObject] return core.callPrivateRPC("eth_getBlockByNumber", payload) proc getNativeChainBalance*(chainId: int, address: string): RpcResponse[JsonNode] {.raises: [Exception].} = diff --git a/ui/app/AppLayouts/stores/RootStore.qml b/ui/app/AppLayouts/stores/RootStore.qml index 8e0f485ab8..ed309e4e89 100644 --- a/ui/app/AppLayouts/stores/RootStore.qml +++ b/ui/app/AppLayouts/stores/RootStore.qml @@ -99,6 +99,10 @@ QtObject { return JSON.parse(walletSectionTransactions.suggestedFees(chainId)) } + function getEstimatedTime(priorityFeePerGas, maxFeePerGas) { + return walletSectionTransactions.getEstimatedTime(priorityFeePerGas, maxFeePerGas) + } + function getChainIdForChat() { return walletSectionTransactions.getChainIdForChat() } diff --git a/ui/imports/shared/controls/GasSelector.qml b/ui/imports/shared/controls/GasSelector.qml index b2a2c9dc91..98aaec8f3f 100644 --- a/ui/imports/shared/controls/GasSelector.qml +++ b/ui/imports/shared/controls/GasSelector.qml @@ -21,11 +21,13 @@ Item { property var getGasGweiValue: function () {} property var getGasEthValue: function () {} property var getFiatValue: function () {} + property var getEstimatedTime: function () {} property string defaultCurrency: "USD" property alias selectedGasPrice: inputGasPrice.text property alias selectedGasLimit: inputGasLimit.text property string defaultGasLimit: "0" property string maxFiatFees: selectedGasFiatValue + root.defaultCurrency.toUpperCase() + property int estimatedTxTimeFlag: Constants.transactionEstimatedTime.unknown property alias selectedTipLimit: inputPerGasTipLimit.text @@ -62,11 +64,15 @@ Item { return } + + Qt.callLater(function () { let ethValue = root.getGasEthValue(inputGasPrice.text, inputGasLimit.text) let fiatValue = root.getFiatValue(ethValue, "ETH", root.defaultCurrency) selectedGasEthValue = ethValue selectedGasFiatValue = fiatValue + root.estimatedTxTimeFlag = root.getEstimatedTime(inputPerGasTipLimit.text, inputGasPrice.text) + }) } function appendError(accum, error, nonBlocking = false) { @@ -111,6 +117,11 @@ Item { } } + Component.onCompleted: { + updateGasEthValue() + checkLimits() + } + function validate() { // causes error on application load without a null check if (!inputGasLimit || !inputGasPrice || !inputPerGasTipLimit) { diff --git a/ui/imports/shared/popups/SendModal.qml b/ui/imports/shared/popups/SendModal.qml index f35add41db..19cce59adf 100644 --- a/ui/imports/shared/popups/SendModal.qml +++ b/ui/imports/shared/popups/SendModal.qml @@ -176,6 +176,7 @@ StatusModal { anchors.top: networkSelector.bottom getGasEthValue: popup.store.getGasEthValue getFiatValue: popup.store.getFiatValue + getEstimatedTime: popup.store.getEstimatedTime defaultCurrency: popup.store.currentCurrency width: stack.width @@ -239,6 +240,7 @@ StatusModal { advancedFooterComponent: SendModalFooter { maxFiatFees: gasSelector.maxFiatFees + estimatedTxTimeFlag: gasSelector.estimatedTxTimeFlag currentGroupPending: popup.contentItem.currentGroup.isPending currentGroupValid: popup.contentItem.currentGroup.isValid isLastGroup: popup.contentItem.isLastGroup diff --git a/ui/imports/shared/views/SendModalFooter.qml b/ui/imports/shared/views/SendModalFooter.qml index 840a10cd9b..bac097b2cb 100644 --- a/ui/imports/shared/views/SendModalFooter.qml +++ b/ui/imports/shared/views/SendModalFooter.qml @@ -12,15 +12,18 @@ import StatusQ.Core.Theme 0.1 Rectangle { id: footer - //% "Unknown" - property string estimatedTime: qsTr("Unknown") property string maxFiatFees: "" + property int estimatedTxTimeFlag: Constants.transactionEstimatedTime.unknown property bool currentGroupPending: true property bool currentGroupValid: false property bool isLastGroup: false signal nextButtonClicked() + onEstimatedTxTimeFlagChanged: { + estimatedTime.text = Utils.getLabelForEstimatedTxTime(estimatedTxTimeFlag) + } + width: parent.width height: 82 radius: 8 @@ -55,9 +58,9 @@ Rectangle { } // To-do not implemented yet StatusBaseText { + id: estimatedTime font.pixelSize: 15 color: Theme.palette.directColor1 - text: estimatedTime wrapMode: Text.WordWrap } } @@ -79,6 +82,7 @@ Rectangle { wrapMode: Text.WordWrap } StatusBaseText { + id: fiatFees font.pixelSize: 15 color: Theme.palette.directColor1 text: maxFiatFees diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 127229167d..1fbae253e9 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -189,6 +189,13 @@ QtObject { readonly property int success: 1 } + readonly property QtObject transactionEstimatedTime: QtObject { + readonly property int unknown: -1 + readonly property int lessThanOneMin: 0 + readonly property int lessThanThreeMins: 1 + readonly property int lessThanFiveMins: 2 + } + readonly property int communityImported: 0 readonly property int communityImportingInProgress: 1 readonly property int communityImportingError: 2 diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index 314135f553..0a96b7d114 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -618,6 +618,19 @@ QtObject { /* Validation section end */ + function getLabelForEstimatedTxTime(estimatedFlag) { + if (estimatedFlag === Constants.transactionEstimatedTime.lessThanOneMin) { + return qsTr("< 1 min") + } + if (estimatedFlag === Constants.transactionEstimatedTime.lessThanThreeMins) { + return qsTr("< 3 mins") + } + if (estimatedFlag === Constants.transactionEstimatedTime.lessThanFiveMins) { + return qsTr("< 5 mins") + } + + return qsTr("> 5 mins") + } function getContactDetailsAsJson(publicKey) { let jsonObj = mainModule.getContactDetailsAsJson(publicKey)