From 285f54dab6407edc5a199e3af3d9aff2a4fd1ef5 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Mon, 5 Jul 2021 08:34:56 -0400 Subject: [PATCH] EIP1559 --- src/app/chat/views/stickers.nim | 10 +- src/app/profile/views/ens_manager.nim | 10 +- src/app/wallet/v1/core.nim | 10 +- src/app/wallet/v1/view.nim | 2 + src/app/wallet/v1/views/gas.nim | 108 ++++--- src/app/wallet/v1/views/transactions.nim | 54 +++- src/nim_status_client.nim | 3 +- ui/app/AppLayouts/Browser/BrowserLayout.qml | 9 +- ui/app/AppLayouts/Chat/ChatColumn.qml | 6 +- .../ChatComponents/SignTransactionModal.qml | 56 +++- .../AcceptTransaction.qml | 2 +- .../SendTransactionButton.qml | 2 +- .../Profile/Sections/Ens/Search.qml | 2 +- .../Sections/Ens/TermsAndConditions.qml | 4 +- ui/app/AppLayouts/Wallet/SendModal.qml | 46 ++- .../TransactionSettingsConfirmationPopup.qml | 258 +++++++++++++++++ ui/app/AppMain.qml | 7 +- ui/shared/GasSelector.qml | 269 ++++++++++++++++-- ui/shared/GasSelectorButton.qml | 8 +- ui/shared/Input.qml | 2 + .../status/StatusETHTransactionModal.qml | 48 +++- .../status/StatusSNTTransactionModal.qml | 45 ++- ui/shared/status/StatusStickerMarket.qml | 4 +- .../status/StatusStickerPackClickPopup.qml | 4 +- vendor/status-lib | 2 +- 25 files changed, 830 insertions(+), 141 deletions(-) create mode 100644 ui/app/AppLayouts/Wallet/TransactionSettingsConfirmationPopup.qml diff --git a/src/app/chat/views/stickers.nim b/src/app/chat/views/stickers.nim index 45d7cbb478..eed424d59f 100644 --- a/src/app/chat/views/stickers.nim +++ b/src/app/chat/views/stickers.nim @@ -118,15 +118,17 @@ QtObject: let estimateResult = Json.decode(estimateJson, tuple[estimate: int, uuid: string]) self.gasEstimateReturned(estimateResult.estimate, estimateResult.uuid) - proc buy*(self: StickersView, packId: int, address: string, price: string, gas: string, gasPrice: string, password: string): string {.slot.} = + proc buy*(self: StickersView, packId: int, address: string, price: string, gas: string, gasPrice: string, maxPriorityFeePerGas: string, maxFeePerGas: string, password: string): string {.slot.} = + let eip1559Enabled = self.status.wallet.isEIP1559Enabled() + try: - validateTransactionInput(address, address, "", price, gas, gasPrice, "", "ok") + validateTransactionInput(address, address, "", price, gas, gasPrice, "", eip1559Enabled, maxPriorityFeePerGas, maxFeePerGas, "ok") except Exception as e: error "Error buying sticker pack", msg = e.msg return "" - + var success: bool - let response = self.status.stickers.buyPack(packId, address, price, gas, gasPrice, password, success) + let response = self.status.stickers.buyPack(packId, address, price, gas, gasPrice, eip1559Enabled, maxPriorityFeePerGas, maxFeePerGas, password, success) # TODO: # check if response["error"] is not null and handle the error diff --git a/src/app/profile/views/ens_manager.nim b/src/app/profile/views/ens_manager.nim index f61457fd8b..1eb8d78786 100644 --- a/src/app/profile/views/ens_manager.nim +++ b/src/app/profile/views/ens_manager.nim @@ -243,10 +243,11 @@ QtObject: if not success: result = 380000 - proc registerENS*(self: EnsManager, username: string, address: string, gas: string, gasPrice: string, password: string): string {.slot.} = + proc registerENS*(self: EnsManager, username: string, address: string, gas: string, gasPrice: string, maxPriorityFeePerGas: string, maxFeePerGas: string, password: string): string {.slot.} = + let eip1559Enabled = self.status.wallet.isEIP1559Enabled() var success: bool let pubKey = self.status.settings.getSetting[:string](Setting.PublicKey, "0x0") - let response = registerUsername(username, pubKey, address, gas, gasPrice, password, success) + let response = registerUsername(username, pubKey, address, gas, gasPrice, eip1559Enabled, maxPriorityFeePerGas, maxFeePerGas, password, success) result = $(%* { "result": %response, "success": %success }) if success: @@ -282,10 +283,11 @@ QtObject: if not success: result = 80000 - proc setPubKey(self: EnsManager, username: string, address: string, gas: string, gasPrice: string, password: string): string {.slot.} = + proc setPubKey*(self: EnsManager, username: string, address: string, gas: string, gasPrice: string, maxPriorityFeePerGas: string, maxFeePerGas: string, password: string): string {.slot.} = + let eip1559Enabled = self.status.wallet.isEIP1559Enabled() var success: bool let pubKey = self.status.settings.getSetting[:string](Setting.PublicKey, "0x0") - let response = setPubKey(username, pubKey, address, gas, gasPrice, password, success) + let response = setPubKey(username, pubKey, address, gas, gasPrice, eip1559Enabled, maxPriorityFeePerGas, maxFeePerGas, password, success) result = $(%* { "result": %response, "success": %success }) if success: self.transactionWasSent(response) diff --git a/src/app/wallet/v1/core.nim b/src/app/wallet/v1/core.nim index 51b8ca7478..39798fb2e4 100644 --- a/src/app/wallet/v1/core.nim +++ b/src/app/wallet/v1/core.nim @@ -60,6 +60,8 @@ proc init*(self: WalletController) = for acc in data.accounts: self.status.wallet.updateAccount(acc) self.status.wallet.checkPendingTransactions(acc, data.blockNumber) + discard self.status.wallet.isEIP1559Enabled(data.blockNumber) + self.status.wallet.setLatestBaseFee(data.baseFeePerGas) self.view.updateView() # TODO: show notification @@ -85,5 +87,9 @@ proc init*(self: WalletController) = let tx = TransactionMinedArgs(e) self.view.transactionCompleted(tx.success, tx.transactionHash, tx.revertReason) -proc checkPendingTransactions*(self: WalletController) = - self.status.wallet.checkPendingTransactions() # TODO: consider doing this in a threadpool task +proc onLogin*(self: WalletController) = + let blockInfo = getLatestBlock() + self.status.wallet.checkPendingTransactions(blockInfo[0]) # TODO: consider doing this in a threadpool task + discard self.status.wallet.isEIP1559Enabled(blockInfo[0]) + self.status.wallet.setLatestBaseFee(blockInfo[1]) + \ No newline at end of file diff --git a/src/app/wallet/v1/view.nim b/src/app/wallet/v1/view.nim index b1fe6154f5..659adf2630 100644 --- a/src/app/wallet/v1/view.nim +++ b/src/app/wallet/v1/view.nim @@ -99,6 +99,8 @@ QtObject: read = getDappBrowserView proc updateView*(self: WalletView) = + self.transactionsView.triggerEIP1559Check() + self.balanceView.setTotalFiatBalance(self.status.wallet.getTotalFiatBalance()) self.balanceView.totalFiatBalanceChanged() diff --git a/src/app/wallet/v1/views/gas.nim b/src/app/wallet/v1/views/gas.nim index 05bfd0a43e..ea3456da3c 100644 --- a/src/app/wallet/v1/views/gas.nim +++ b/src/app/wallet/v1/views/gas.nim @@ -1,9 +1,10 @@ import atomics, strformat, strutils, sequtils, json, std/wrapnils, parseUtils, chronicles, web3/[ethtypes, conversions], stint -import NimQml, json, sequtils, chronicles, strutils, strformat, json +import NimQml, json, sequtils, chronicles, strutils, strformat, json, math import status/[status, wallet, utils], - status/types/[gas_prediction] + status/types/[gas_prediction], + status/libstatus/wallet as status_wallet import ../../../../app_service/[main] import ../../../../app_service/tasks/[qt, threadpool] @@ -17,7 +18,10 @@ type const getGasPredictionsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = let arg = decode[GasPredictionsTaskArg](argEncoded) - output = %getGasPricePredictions() + response = status_wallet.getGasPrice().parseJson + var output = "0" + if response.hasKey("result"): + output = $fromHex(Stuint[256], response["result"].getStr) arg.finish(output) proc getGasPredictions[T](self: T, slot: string) = @@ -35,10 +39,7 @@ QtObject: type GasView* = ref object of QObject status: Status appService: AppService - safeLowGasPrice: string - standardGasPrice: string - fastGasPrice: string - fastestGasPrice: string + gasPrice: string defaultGasLimit: string proc setup(self: GasView) = self.QObject.setup @@ -48,10 +49,7 @@ QtObject: new(result, delete) result.status = status result.appService = appService - result.safeLowGasPrice = "0" - result.standardGasPrice = "0" - result.fastGasPrice = "0" - result.fastestGasPrice = "0" + result.gasPrice = "0" result.defaultGasLimit = "21000" result.setup @@ -89,39 +87,69 @@ QtObject: else: result = $(%* { "result": "-1", "success": %success, "error": { "message": %response } }) - proc gasPricePredictionsChanged*(self: GasView) {.signal.} + proc gasPriceChanged*(self: GasView) {.signal.} - proc getGasPricePredictions*(self: GasView) {.slot.} = - self.getGasPredictions("getGasPricePredictionsResult") + proc getGasPrice*(self: GasView) {.slot.} = + if not self.status.wallet.isEIP1559Enabled(): + self.getGasPredictions("getGasPriceResult") - proc getGasPricePredictionsResult(self: GasView, gasPricePredictionsJson: string) {.slot.} = - let prediction = Json.decode(gasPricePredictionsJson, GasPricePrediction) - self.safeLowGasPrice = fmt"{prediction.safeLow:.3f}" - self.standardGasPrice = fmt"{prediction.standard:.3f}" - self.fastGasPrice = fmt"{prediction.fast:.3f}" - self.fastestGasPrice = fmt"{prediction.fastest:.3f}" - self.gasPricePredictionsChanged() + proc getGasPriceResult(self: GasView, gasPrice: string) {.slot.} = + let p = parseFloat(wei2gwei(gasPrice)) + self.gasPrice = fmt"{p:.3f}" + self.gasPriceChanged() - proc safeLowGasPrice*(self: GasView): string {.slot.} = result = ?.self.safeLowGasPrice - QtProperty[string] safeLowGasPrice: - read = safeLowGasPrice - notify = gasPricePredictionsChanged - - proc standardGasPrice*(self: GasView): string {.slot.} = result = ?.self.standardGasPrice - QtProperty[string] standardGasPrice: - read = standardGasPrice - notify = gasPricePredictionsChanged - - proc fastGasPrice*(self: GasView): string {.slot.} = result = ?.self.fastGasPrice - QtProperty[string] fastGasPrice: - read = fastGasPrice - notify = gasPricePredictionsChanged - - proc fastestGasPrice*(self: GasView): string {.slot.} = result = ?.self.fastestGasPrice - QtProperty[string] fastestGasPrice: - read = fastestGasPrice - notify = gasPricePredictionsChanged + proc gasPrice*(self: GasView): string {.slot.} = result = ?.self.gasPrice + QtProperty[string] gasPrice: + read = gasPrice + notify = gasPriceChanged proc defaultGasLimit*(self: GasView): string {.slot.} = result = ?.self.defaultGasLimit QtProperty[string] defaultGasLimit: read = defaultGasLimit + + proc maxPriorityFeePerGas*(self: GasView): string {.slot.} = + result = self.status.wallet.maxPriorityFeePerGas() + debug "Max priority fee per gas", value=result + + proc suggestedFees*(self: GasView): string {.slot.} = + #[ + 0. priority tip always same, the value returned by eth_maxPriorityFeePerGas + 1. slow fee 10th percentile base fee (last 100 blocks) + eth_maxPriorityFeePerGas + 2. normal fee. + if 20th_percentile <= current_base_fee <= 80th_percentile then fee = current_base_fee + eth_maxPriorityFeePerGas. + if current_base_fee < 20th_percentile then fee = 20th_percentile + eth_maxPriorityFeePerGas + if current_base_fee > 80th_percentile then fee = 80th_percentile + eth_maxPriorityFeePerGas + The idea is to avoid setting too low base fee when price is in a dip and also to avoid overpaying on peak. Specific percentiles can be revisit later, it doesn't need to be symmetric because we are mostly interested in not getting stuck and overpaying might not be a huge issue here. + 3. fast fee: current_base_fee + eth_maxPriorityFeePerGas + ]# + + let maxPriorityFeePerGas = self.status.wallet.maxPriorityFeePerGas().u256 + let feeHistory = self.status.wallet.feeHistory(101) + let baseFee = self.status.wallet.getLatestBaseFee().u256 + let gasPrice = self.status.wallet.getGasPrice().u256 + + let perc10 = feeHistory[ceil(10/100 * feeHistory.len.float).int - 1] + let perc20 = feeHistory[ceil(20/100 * feeHistory.len.float).int - 1] + let perc80 = feeHistory[ceil(80/100 * feeHistory.len.float).int - 1] + + let maxFeePerGasM = if baseFee >= perc20 and baseFee <= perc80: + baseFee + maxPriorityFeePerGas + elif baseFee < perc20: + perc20 + maxPriorityFeePerGas + else: + perc80 + maxPriorityFeePerGas + + result = $(%* { + "gasPrice": $gasPrice, + "baseFee": parseFloat(wei2gwei($baseFee)), + "maxPriorityFeePerGas": parseFloat(wei2gwei($maxPriorityFeePerGas)), + "maxFeePerGasL": parseFloat(wei2gwei($(perc10 + maxPriorityFeePerGas))), + "maxFeePerGasM": parseFloat(wei2gwei($(maxFeePerGasM))), + "maxFeePerGasH": parseFloat(wei2gwei($(baseFee + maxPriorityFeePerGas))) + }) + + QtProperty[string] maxPriorityFeePerGas: + read = maxPriorityFeePerGas + + QtProperty[string] suggestedFees: + read = suggestedFees \ No newline at end of file diff --git a/src/app/wallet/v1/views/transactions.nim b/src/app/wallet/v1/views/transactions.nim index 8053d0e984..e866710c0b 100644 --- a/src/app/wallet/v1/views/transactions.nim +++ b/src/app/wallet/v1/views/transactions.nim @@ -24,6 +24,9 @@ type value: string gas: string gasPrice: string + isEIP1559Enabled: bool + maxPriorityFeePerGas: string + maxFeePerGas: string password: string uuid: string WatchTransactionTaskArg = ref object of QObjectTaskArg @@ -35,19 +38,20 @@ const sendTransactionTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = success: bool response: string if arg.assetAddress != ZERO_ADDRESS and not arg.assetAddress.isEmptyOrWhitespace: - response = wallet.sendTokenTransaction(arg.from_addr, arg.to, arg.assetAddress, arg.value, arg.gas, arg.gasPrice, arg.password, success) + response = wallet.sendTokenTransaction(arg.from_addr, arg.to, arg.assetAddress, arg.value, arg.gas, arg.gasPrice, arg.isEIP1559Enabled, arg.maxPriorityFeePerGas, arg.maxFeePerGas, arg.password, success) else: - response = wallet.sendTransaction(arg.from_addr, arg.to, arg.value, arg.gas, arg.gasPrice, arg.password, success) + response = wallet.sendTransaction(arg.from_addr, arg.to, arg.value, arg.gas, arg.gasPrice, arg.isEIP1559Enabled, arg.maxPriorityFeePerGas, arg.maxFeePerGas, arg.password, success) let output = %* { "result": %response, "success": %success, "uuid": %arg.uuid } arg.finish(output) -proc sendTransaction[T](self: T, slot: string, from_addr: string, to: string, assetAddress: string, value: string, gas: string, gasPrice: string, password: string, uuid: string) = +proc sendTransaction[T](self: T, slot: string, from_addr: string, to: string, assetAddress: string, value: string, gas: string, gasPrice: string, isEIP1559Enabled: bool, maxPriorityFeePerGas: string, maxFeePerGas: string, password: string, uuid: string) = let arg = SendTransactionTaskArg( tptr: cast[ByteAddress](sendTransactionTask), vptr: cast[ByteAddress](self.vptr), slot: slot, from_addr: from_addr, to: to, assetAddress: assetAddress, value: value, gas: gas, - gasPrice: gasPrice, password: password, uuid: uuid + gasPrice: gasPrice, password: password, uuid: uuid, + isEIP1559Enabled: isEIP1559Enabled, maxPriorityFeePerGas: maxPriorityFeePerGas, maxFeePerGas: maxFeePerGas ) self.appService.threadpool.start(arg) @@ -110,23 +114,24 @@ QtObject: if txHash != "": self.watchTransaction("transactionWatchResultReceived", txHash) - proc sendTransaction*(self: TransactionsView, from_addr: string, to: string, assetAddress: string, value: string, gas: string, gasPrice: string, password: string, uuid: string) {.slot.} = - self.sendTransaction("transactionSent", from_addr, to, assetAddress, value, gas, gasPrice, password, uuid) + proc sendTransaction*(self: TransactionsView, from_addr: string, to: string, assetAddress: string, value: string, gas: string, gasPrice: string,eip1559Enabled: bool, maxPriorityFeePerGas: string, maxFeePerGas: string, password: string, uuid: string) {.slot.} = + self.sendTransaction("transactionSent", from_addr, to, assetAddress, value, gas, gasPrice, eip1559Enabled, maxPriorityFeePerGas, maxFeePerGas, password, uuid) - - proc transferEth*(self: TransactionsView, from_addr: string, to_addr: string, value: string, gas: string, gasPrice: string, password: string, uuid: string): bool {.slot.} = + proc transferEth*(self: TransactionsView, from_addr: string, to_addr: string, value: string, gas: string, gasPrice: string, maxPriorityFeePerGas: string, maxFeePerGas: string, password: string, uuid: string): bool {.slot.} = try: - validateTransactionInput(from_addr, to_addr, "", value, gas, gasPrice, "", uuid) - self.sendTransaction("transactionSent", from_addr, to_addr, ZERO_ADDRESS, value, gas, gasPrice, password, uuid) + let eip1559Enabled = self.status.wallet.isEIP1559Enabled() + validateTransactionInput(from_addr, to_addr, "", value, gas, gasPrice, "", eip1559Enabled, maxPriorityFeePerGas, maxFeePerGas, uuid) + self.sendTransaction("transactionSent", from_addr, to_addr, ZERO_ADDRESS, value, gas, gasPrice, eip1559Enabled, maxPriorityFeePerGas, maxFeePerGas, password, uuid) except Exception as e: error "Error sending eth transfer transaction", msg = e.msg return false return true - proc transferTokens*(self: TransactionsView, from_addr: string, to_addr: string, assetAddress: string, value: string, gas: string, gasPrice: string, password: string, uuid: string): bool {.slot.} = + proc transferTokens*(self: TransactionsView, from_addr: string, to_addr: string, assetAddress: string, value: string, gas: string, gasPrice: string, maxPriorityFeePerGas: string, maxFeePerGas: string, password: string, uuid: string): bool {.slot.} = try: - validateTransactionInput(from_addr, to_addr, assetAddress, value, gas, gasPrice, "", uuid) - self.sendTransaction("transactionSent", from_addr, to_addr, assetAddress, value, gas, gasPrice, password, uuid) + let eip1559Enabled = self.status.wallet.isEIP1559Enabled() + validateTransactionInput(from_addr, to_addr, assetAddress, value, gas, gasPrice, "", eip1559Enabled, maxPriorityFeePerGas, maxFeePerGas, uuid) + self.sendTransaction("transactionSent", from_addr, to_addr, assetAddress, value, gas, gasPrice, eip1559Enabled, maxPriorityFeePerGas, maxFeePerGas, password, uuid) except Exception as e: error "Error sending token transfer transaction", msg = e.msg return false @@ -146,3 +151,26 @@ QtObject: discard #TODO: Ask Simon if should we show an error popup indicating the trx wasn't mined in 10m or something proc transactionCompleted*(self: TransactionsView, success: bool, txHash: string, revertReason: string = "") {.signal.} + + proc triggerEIP1559Check*(self: TransactionsView) {.signal.} + + proc isEIP1559Enabled(self: TransactionsView): bool {.slot.} = + return self.status.wallet.isEIP1559Enabled() + + proc getLatestBaseFee(self: TransactionsView): string {.slot.} = + var baseFeeWei:string = self.status.wallet.getLatestBaseFee() + var baseFeeGwei:string = wei2Gwei(baseFeeWei) + var unit:string = "wei" + var amount = baseFeeWei + if parseFloat(baseFeeGwei) > 1: + unit = "gwei" + amount = baseFeeGwei + return $(%*{"gwei": baseFeeGwei, "amount": amount, "unit": unit}) + + QtProperty[bool] isEIP1559Enabled: + read = isEIP1559Enabled + notify = triggerEIP1559Check + + QtProperty[string] latestBaseFee: + read = getLatestBaseFee + notify = triggerEIP1559Check \ No newline at end of file diff --git a/src/nim_status_client.nim b/src/nim_status_client.nim index 9db91b2f53..61e9ebcd0a 100644 --- a/src/nim_status_client.nim +++ b/src/nim_status_client.nim @@ -198,7 +198,8 @@ proc mainProc() = utilsController.init() browserController.init() node.init() - wallet.checkPendingTransactions() + + wallet.onLogin() # this should be the last defer in the scope defer: diff --git a/ui/app/AppLayouts/Browser/BrowserLayout.qml b/ui/app/AppLayouts/Browser/BrowserLayout.qml index 5ae78b2c39..fd7f5765c5 100644 --- a/ui/app/AppLayouts/Browser/BrowserLayout.qml +++ b/ui/app/AppLayouts/Browser/BrowserLayout.qml @@ -77,7 +77,7 @@ Rectangle { } // TODO we'll need a new dialog at one point because this one is not using the same call, but it's good for now -property Component sendTransactionModalComponent: SignTransactionModal {} + property Component sendTransactionModalComponent: SignTransactionModal {} property Component signMessageModalComponent: SignMessageModal {} @@ -190,9 +190,11 @@ property Component sendTransactionModalComponent: SignTransactionModal {} }); // TODO change sendTransaction function to the postMessage one - sendDialog.sendTransaction = function (selectedGasLimit, selectedGasPrice, enteredPassword) { + sendDialog.sendTransaction = function (selectedGasLimit, selectedGasPrice, selectedTipLimit, selectedOverallLimit, enteredPassword) { request.payload.selectedGasLimit = selectedGasLimit request.payload.selectedGasPrice = selectedGasPrice + request.payload.selectedTipLimit = selectedTipLimit + request.payload.selectedOverallLimit = selectedOverallLimit request.payload.password = enteredPassword request.payload.params[0].value = value @@ -229,7 +231,7 @@ property Component sendTransactionModalComponent: SignTransactionModal {} } sendDialog.open(); - walletModel.gasView.getGasPricePredictions() + walletModel.gasView.getGasPrice() } else if (request.type === Constants.web3SendAsyncReadOnly && ["eth_sign", "personal_sign", "eth_signTypedData", "eth_signTypedData_v3"].indexOf(request.payload.method) > -1) { const signDialog = signMessageModalComponent.createObject(browserWindow, { request, @@ -516,6 +518,7 @@ property Component sendTransactionModalComponent: SignTransactionModal {} anchors.top: parent.top anchors.topMargin: browserHeader.height focus: true + url: "https://dap.ps" webChannel: channel onLinkHovered: function(hoveredUrl) { if (hoveredUrl === "") diff --git a/ui/app/AppLayouts/Chat/ChatColumn.qml b/ui/app/AppLayouts/Chat/ChatColumn.qml index 05bef5ee5c..f2021ef49f 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn.qml @@ -498,7 +498,7 @@ Item { SendModal { id: sendTransactionWithEns onOpened: { - walletModel.gasView.getGasPricePredictions() + walletModel.gasView.getGasPrice() } onClosed: { txModalLoader.closed() @@ -622,8 +622,8 @@ Item { Connections { target: chatsModel.stickers onTransactionWasSent: { - //% "Transaction pending" - toastMessage.title = qsTrId("transaction-pending") + //% "Transaction pending..." + toastMessage.title = qsTr("Transaction pending...") toastMessage.source = "../../../img/loading.svg" toastMessage.iconColor = Style.current.primary toastMessage.iconRotates = true diff --git a/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/SignTransactionModal.qml b/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/SignTransactionModal.qml index 50bd8168b3..53c1fab790 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/SignTransactionModal.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/ChatComponents/SignTransactionModal.qml @@ -5,6 +5,7 @@ import QtQuick.Dialogs 1.3 import "../../../../../imports" import "../../../../../shared" import "../../../../../shared/status" +import "../../../Wallet/" ModalPopup { property var selectedAccount @@ -18,7 +19,7 @@ ModalPopup { property alias transactionSigner: transactionSigner - property var sendTransaction: function(selectedGasLimit, selectedGasPrice, enteredPassword) { + property var sendTransaction: function(selectedGasLimit, selectedGasPrice, selectedTipLimit, selectedOveralLimit, enteredPassword) { let success = false if(root.selectedAsset.address == Constants.zeroAddress){ success = walletModel.transactionsView.transferEth( @@ -26,7 +27,9 @@ ModalPopup { selectRecipient.selectedRecipient.address, root.selectedAmount, selectedGasLimit, - selectedGasPrice, + gasSelector.eip1599Enabled ? "" : gasSelector.selectedGasPrice, + gasSelector.selectedTipLimit, + gasSelector.selectedOverallLimit, enteredPassword, stack.uuid) } else { @@ -36,7 +39,9 @@ ModalPopup { root.selectedAsset.address, root.selectedAmount, selectedGasLimit, - selectedGasPrice, + gasSelector.eip1599Enabled ? "" : gasSelector.selectedGasPrice, + gasSelector.selectedTipLimit, + gasSelector.selectedOverallLimit, enteredPassword, stack.uuid) } @@ -52,7 +57,7 @@ ModalPopup { //% "Send" title: qsTrId("command-button-send") - height: 504 + height: 540 property MessageDialog sendingError: MessageDialog { id: sendingError @@ -60,7 +65,6 @@ ModalPopup { title: qsTrId("error-sending-the-transaction") icon: StandardIcon.Critical standardButtons: StandardButton.Ok - onAccepted: root.close() } onClosed: { @@ -122,8 +126,7 @@ ModalPopup { id: groupSelectGas //% "Network fee" headerText: qsTrId("network-fee") - //% "Preview" - footerText: qsTrId("preview") + footerText: qsTr("Continue") showNextBtn: false onBackClicked: function() { stack.pop() @@ -131,8 +134,7 @@ ModalPopup { GasSelector { id: gasSelector anchors.topMargin: Style.current.bigPadding - slowestGasPrice: parseFloat(walletModel.gasView.safeLowGasPrice) - fastestGasPrice: parseFloat(walletModel.gasView.fastestGasPrice) + gasPrice: parseFloat(walletModel.gasView.gasPrice) getGasEthValue: walletModel.gasView.getGasEthValue getFiatValue: walletModel.balanceView.getFiatValue defaultCurrency: walletModel.balanceView.defaultCurrency @@ -144,6 +146,7 @@ ModalPopup { root.selectedAsset && root.selectedAsset.address && root.selectedAmount)) { selectedGasLimit = 250000 + defaultGasLimit = selectedGasLimit return } @@ -164,6 +167,7 @@ ModalPopup { return } selectedGasLimit = gasEstimate.result + defaultGasLimit = selectedGasLimit }) } GasValidator { @@ -267,6 +271,14 @@ ModalPopup { stack.back() } } + + Component { + id: transactionSettingsConfirmationPopupComponent + TransactionSettingsConfirmationPopup { + + } + } + StatusButton { id: btnNext anchors.right: parent.right @@ -279,9 +291,33 @@ ModalPopup { if (validity.isValid && !validity.isPending) { if (stack.isLastGroup) { return root.sendTransaction(gasSelector.selectedGasLimit, - gasSelector.selectedGasPrice, + gasSelector.eip1599Enabled ? "" : gasSelector.selectedGasPrice, + gasSelector.selectedTipLimit, + gasSelector.selectedOverallLimit, transactionSigner.enteredPassword) } + + if(gasSelector.eip1599Enabled && stack.currentGroup === groupSelectGas && gasSelector.advancedMode){ + if(gasSelector.showPriceLimitWarning || gasSelector.showTipLimitWarning){ + openPopup(transactionSettingsConfirmationPopupComponent, { + currentBaseFee: gasSelector.latestBaseFeeGwei, + currentMinimumTip: gasSelector.perGasTipLimitFloor, + currentAverageTip: gasSelector.perGasTipLimitAverage, + tipLimit: gasSelector.selectedTipLimit, + suggestedTipLimit: gasSelector.perGasTipLimitFloor, // TODO: + priceLimit: gasSelector.selectedOverallLimit, + suggestedPriceLimit: gasSelector.latestBaseFeeGwei + gasSelector.perGasTipLimitFloor, + showPriceLimitWarning: gasSelector.showPriceLimitWarning, + showTipLimitWarning: gasSelector.showTipLimitWarning, + onConfirm: function(){ + stack.next(); + } + }) + return + } + } + + if (typeof stack.currentGroup.onNextClicked === "function") { return stack.currentGroup.onNextClicked() } diff --git a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/AcceptTransaction.qml b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/AcceptTransaction.qml index 010d7bbca1..4a1929b3e1 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/AcceptTransaction.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/AcceptTransaction.qml @@ -89,7 +89,7 @@ Item { id: signTxComponent SignTransactionModal { onOpened: { - walletModel.gasView.getGasPricePredictions() + walletModel.gasView.getGasPrice() } onClosed: { destroy(); diff --git a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/SendTransactionButton.qml b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/SendTransactionButton.qml index 88b6c21e12..4fbd1b514e 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/SendTransactionButton.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/MessageComponents/TransactionComponents/SendTransactionButton.qml @@ -46,7 +46,7 @@ Item { id: signTxComponent SignTransactionModal { onOpened: { - walletModel.gasView.getGasPricePredictions() + walletModel.gasView.getGasPrice() } onClosed: { destroy(); diff --git a/ui/app/AppLayouts/Profile/Sections/Ens/Search.qml b/ui/app/AppLayouts/Profile/Sections/Ens/Search.qml index 8a3b8b0aee..3d8bb2841e 100644 --- a/ui/app/AppLayouts/Profile/Sections/Ens/Search.qml +++ b/ui/app/AppLayouts/Profile/Sections/Ens/Search.qml @@ -51,7 +51,7 @@ Item { id: transactionDialogComponent StatusETHTransactionModal { onOpened: { - walletModel.gasView.getGasPricePredictions() + walletModel.gasView.getGasPrice() } title: qsTr("Connect username with your pubkey") onClosed: { diff --git a/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml b/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml index c58e894972..9d25dea54a 100644 --- a/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml +++ b/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml @@ -39,10 +39,12 @@ Item { if (username === "" || !selectedAccount) return 380000; return profileModel.ens.registerENSGasEstimate(username, selectedAccount.address) } - onSendTransaction: function(selectedAddress, gasLimit, gasPrice, password) { + onSendTransaction: function(selectedAddress, gasLimit, gasPrice, tipLimit, overallLimit, password) { return profileModel.ens.registerENS(username, selectedAddress, gasLimit, + tipLimit, + overallLimit, gasPrice, password) } diff --git a/ui/app/AppLayouts/Wallet/SendModal.qml b/ui/app/AppLayouts/Wallet/SendModal.qml index bfd2e12c19..c63ad319ab 100644 --- a/ui/app/AppLayouts/Wallet/SendModal.qml +++ b/ui/app/AppLayouts/Wallet/SendModal.qml @@ -34,7 +34,9 @@ ModalPopup { selectRecipient.selectedRecipient.address, txtAmount.selectedAmount, gasSelector.selectedGasLimit, - gasSelector.selectedGasPrice, + gasSelector.eip1599Enabled ? "" : gasSelector.selectedGasPrice, + gasSelector.selectedTipLimit, + gasSelector.selectedOverallLimit, transactionSigner.enteredPassword, stack.uuid) } else { @@ -44,7 +46,9 @@ ModalPopup { txtAmount.selectedAsset.address, txtAmount.selectedAmount, gasSelector.selectedGasLimit, - gasSelector.selectedGasPrice, + gasSelector.eip1599Enabled ? "" : gasSelector.selectedGasPrice, + gasSelector.selectedTipLimit, + gasSelector.selectedOverallLimit, transactionSigner.enteredPassword, stack.uuid) } @@ -53,7 +57,6 @@ ModalPopup { //% "Invalid transaction parameters" sendingError.text = qsTrId("invalid-transaction-parameters") sendingError.open() - root.close() } } @@ -111,7 +114,7 @@ ModalPopup { //% "Send" headerText: qsTrId("command-button-send") //% "Preview" - footerText: qsTrId("preview") + footerText: qsTr("Continue") AssetAndAmountInput { id: txtAmount @@ -127,11 +130,11 @@ ModalPopup { id: gasSelector anchors.top: txtAmount.bottom anchors.topMargin: Style.current.bigPadding * 2 - slowestGasPrice: parseFloat(walletModel.gasView.safeLowGasPrice) - fastestGasPrice: parseFloat(walletModel.gasView.fastestGasPrice) + gasPrice: parseFloat(walletModel.gasView.gasPrice) getGasEthValue: walletModel.gasView.getGasEthValue getFiatValue: walletModel.balanceView.getFiatValue defaultCurrency: walletModel.balanceView.defaultCurrency + width: stack.width property var estimateGas: Backpressure.debounce(gasSelector, 600, function() { if (!(selectFromAccount.selectedAccount && selectFromAccount.selectedAccount.address && @@ -151,7 +154,9 @@ ModalPopup { console.warn(qsTrId("error-estimating-gas---1").arg(gasEstimate.error.message)) return } + selectedGasLimit = gasEstimate.result + defaultGasLimit = selectedGasLimit }) } GasValidator { @@ -222,6 +227,14 @@ ModalPopup { stack.back() } } + + Component { + id: transactionSettingsConfirmationPopupComponent + TransactionSettingsConfirmationPopup { + + } + } + StatusButton { id: btnNext anchors.right: parent.right @@ -235,6 +248,27 @@ ModalPopup { if (stack.isLastGroup) { return root.sendTransaction() } + + if(gasSelector.eip1599Enabled && stack.currentGroup === group2 && gasSelector.advancedMode){ + if(gasSelector.showPriceLimitWarning || gasSelector.showTipLimitWarning){ + openPopup(transactionSettingsConfirmationPopupComponent, { + currentBaseFee: gasSelector.latestBaseFeeGwei, + currentMinimumTip: gasSelector.perGasTipLimitFloor, + currentAverageTip: gasSelector.perGasTipLimitAverage, + tipLimit: gasSelector.selectedTipLimit, + suggestedTipLimit: gasSelector.perGasTipLimitFloor, + priceLimit: gasSelector.selectedOverallLimit, + suggestedPriceLimit: gasSelector.latestBaseFeeGwei + gasSelector.perGasTipLimitFloor, + showPriceLimitWarning: gasSelector.showPriceLimitWarning, + showTipLimitWarning: gasSelector.showTipLimitWarning, + onConfirm: function(){ + stack.next(); + } + }) + return + } + } + stack.next() } } diff --git a/ui/app/AppLayouts/Wallet/TransactionSettingsConfirmationPopup.qml b/ui/app/AppLayouts/Wallet/TransactionSettingsConfirmationPopup.qml new file mode 100644 index 0000000000..e0f16f5d25 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/TransactionSettingsConfirmationPopup.qml @@ -0,0 +1,258 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 +import "../../../imports" +import "../../../shared/status" +import "../../../shared" + +ModalPopup { + id: popup + + height: 300 + (showPriceLimitWarning ? 65 : 0) + (showTipLimitWarning ? 65 : 0) + width: 400 + title: qsTr("Are you sure?") + + property var onConfirm: function(){} + + property double currentBaseFee: 0 + property double currentMinimumTip: 0 + property double currentAverageTip: 0 + property double tipLimit: 0 + property double suggestedTipLimit: 0 + property double priceLimit: 0 + property double suggestedPriceLimit: 0 + + property bool showPriceLimitWarning: false + property bool showTipLimitWarning: false + + Column { + id: content + width: 450 + height: parent.height + spacing: 10 + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Style.current.smallPadding + text: qsTr("Your priority fee is below our suggested parameters.") + font.pixelSize: 13 + wrapMode: Text.WordWrap + color: Style.current.secondaryText + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Style.current.smallPadding + height: 20 + + StyledText { + text: qsTr("Current base fee") + font.pixelSize: 13 + width: 190 + anchors.left: parent.left + } + + StyledText { + text: qsTr("%1 Gwei").arg(currentBaseFee) + font.pixelSize: 13 + width: 190 + anchors.right: parent.right + } + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Style.current.smallPadding + height: 20 + + StyledText { + text: qsTr("Current minimum tip") + font.pixelSize: 13 + width: 190 + anchors.left: parent.left + } + + StyledText { + text: qsTr("%1 Gwei").arg(currentMinimumTip) + font.pixelSize: 13 + width: 190 + anchors.right: parent.right + } + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Style.current.smallPadding + height: 20 + + StyledText { + text: qsTr("Current average tip") + font.pixelSize: 13 + width: 190 + anchors.left: parent.left + } + + StyledText { + text: qsTr("%1 Gwei").arg(currentAverageTip) + font.pixelSize: 13 + width: 190 + anchors.right: parent.right + } + } + + Rectangle { + id: tipLimitRect + width: 368 + visible: showTipLimitWarning + height: visible ? 70 : 0 + radius: 8 + color: Style.current.backgroundHoverLight + + Column { + anchors.top: parent.top + anchors.topMargin: Style.current.smallPadding + anchors.left: parent.left + anchors.leftMargin: Style.current.smallPadding + height: 100 + width: 450 - Style.current.smallPadding + spacing: 10 + + Item { + anchors.left: parent.left + anchors.right: parent.right + height: 20 + StyledText { + text: qsTr("Your tip limit") + font.pixelSize: 13 + width: 190 + anchors.left: parent.left + color: Style.current.red + } + + StyledText { + text: qsTr("%1 Gwei").arg(tipLimit) + font.pixelSize: 13 + width: 190 + anchors.right: parent.right + color: Style.current.red + } + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + height: 20 + + StyledText { + text: qsTr("Suggested minimum tip") + font.pixelSize: 13 + width: 190 + anchors.left: parent.left + } + + StyledText { + text: qsTr("%1 Gwei").arg(suggestedTipLimit) + font.pixelSize: 13 + width: 190 + anchors.right: parent.right + } + } + } + + } + + Rectangle { + id: minPriceLimitRect + width: 368 + visible: showPriceLimitWarning + height: visible ? 70 : 0 + radius: 8 + color: Style.current.backgroundHoverLight + + Column { + anchors.top: parent.top + anchors.topMargin: Style.current.smallPadding + anchors.left: parent.left + anchors.leftMargin: Style.current.smallPadding + height: 100 + width: 450 - Style.current.smallPadding + spacing: 10 + + Item { + anchors.left: parent.left + anchors.right: parent.right + height: 20 + StyledText { + text: qsTr("Your price limit") + font.pixelSize: 13 + width: 190 + anchors.left: parent.left + color: Style.current.red + } + + StyledText { + text: qsTr("%1 Gwei").arg(priceLimit) + font.pixelSize: 13 + width: 190 + anchors.right: parent.right + color: Style.current.red + } + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + height: 20 + + StyledText { + text: qsTr("Suggested minimum price limit") + font.pixelSize: 13 + width: 190 + anchors.left: parent.left + } + + StyledText { + text: qsTr("%1 Gwei").arg(suggestedPriceLimit) + font.pixelSize: 13 + width: 190 + anchors.right: parent.right + } + } + } + } + + } + + footer: Item { + id: footerContainer + width: parent.width + + StatusButton { + id: cancelButton + anchors.right: confirmButton.left + anchors.rightMargin: Style.current.smallPadding + text: qsTr("Change Limit") + anchors.bottom: parent.bottom + onClicked: popup.destroy() + } + + StatusButton { + id: confirmButton + type: "warn" + anchors.right: parent.right + anchors.rightMargin: Style.current.smallPadding + text: qsTr("Continue anyway") + anchors.bottom: parent.bottom + onClicked: { + popup.onConfirm(); + popup.destroy(); + } + } + } +} + + + diff --git a/ui/app/AppMain.qml b/ui/app/AppMain.qml index 3553d05fcb..585e75ea1e 100644 --- a/ui/app/AppMain.qml +++ b/ui/app/AppMain.qml @@ -743,16 +743,15 @@ Item { } } - - ToastMessage { id: toastMessage } + // Add SendModal here as it is used by the Wallet as well as the Browser Loader { id: sendModal active: false - + function open() { this.active = true this.item.open() @@ -763,7 +762,7 @@ Item { } sourceComponent: SendModal { onOpened: { - walletModel.gasView.getGasPricePredictions() + walletModel.gasView.getGasPrice() } onClosed: { sendModal.closed() diff --git a/ui/shared/GasSelector.qml b/ui/shared/GasSelector.qml index 2c80aa1969..de4d4b0a7c 100644 --- a/ui/shared/GasSelector.qml +++ b/ui/shared/GasSelector.qml @@ -11,14 +11,30 @@ Item { height: Style.current.smallPadding + prioritytext.height + (advancedMode ? advancedModeItemGroup.height : selectorButtons.height) - property double slowestGasPrice: 0 - property double fastestGasPrice: 100 - property double stepSize: ((root.fastestGasPrice - root.slowestGasPrice) / 10).toFixed(1) + property double gasPrice: 0 + + + property bool eip1599Enabled: walletModel.transactionsView.isEIP1559Enabled + property var suggestedFees: JSON.parse(walletModel.gasView.suggestedFees) + property var latestBaseFee: JSON.parse(walletModel.transactionsView.latestBaseFee) + + property double latestBaseFeeGwei: { + if (!eip1599Enabled) return 0; + return parseFloat(latestBaseFee.gwei) + } + + property var getGasGweiValue: function () {} property var getGasEthValue: function () {} property var getFiatValue: function () {} property string defaultCurrency: "USD" property alias selectedGasPrice: inputGasPrice.text property alias selectedGasLimit: inputGasLimit.text + property string defaultGasLimit: "0" + + + property alias selectedTipLimit: inputPerGasTipLimit.text + property alias selectedOverallLimit: inputGasPrice.text + property double selectedGasEthValue property double selectedGasFiatValue //% "Must be greater than 0" @@ -30,13 +46,26 @@ Item { property bool isValid: true readonly property string uuid: Utils.uuid() - property bool advancedMode: false + property bool advancedMode: true // TODO: change to false once EIP1559 suggestions are revised + + // TODO: change these values false once EIP1559 suggestions are revised + property double perGasTipLimitFloor: 1 // Matches status-react minimum-priority-fee + property double perGasTipLimitAverage: formatDec(suggestedFees.maxPriorityFeePerGas, 2) // 1.5 // Matches status-react average-priority-fee + + + property bool showPriceLimitWarning : false + property bool showTipLimitWarning : false + + function formatDec(num, dec){ + return Math.round((num + Number.EPSILON) * Math.pow(10, dec)) / Math.pow(10, dec) + } function updateGasEthValue() { // causes error on application load without this null check if (!inputGasPrice || !inputGasLimit) { return } + let ethValue = root.getGasEthValue(inputGasPrice.text, inputGasLimit.text) let fiatValue = root.getFiatValue(ethValue, "ETH", root.defaultCurrency) @@ -44,39 +73,114 @@ Item { selectedGasFiatValue = fiatValue } - Component.onCompleted: updateGasEthValue() + function appendError(accum, error, nonBlocking = false) { + return accum + ` ${error}.` + } + + + function checkLimits(){ + if(!eip1599Enabled) return; + + let inputTipLimit = parseFloat(inputPerGasTipLimit.text || "0.00") + let inputOverallLimit = parseFloat(inputGasPrice.text || "0.00") + let gasLimit = parseInt(inputGasLimit.text, 10) + errorsText.text = ""; + + showPriceLimitWarning = false + showTipLimitWarning = false + + let errorMsg = ""; + + if(gasLimit < 21000) { + errorMsg = appendError(errorMsg, qsTr("Min 21000 units")) + } else if (gasLimit < parseInt(defaultGasLimit)){ + errorMsg = appendError(errorMsg, qsTr("Not enough gas").arg(perGasTipLimitAverage), true) + } + + // Per-gas tip limit rules + if(inputTipLimit < perGasTipLimitFloor){ + errorMsg = appendError(errorMsg, qsTr("Miners will currently not process transactions with a tip below %1 Gwei, the average is %2 Gwei").arg(perGasTipLimitFloor).arg(perGasTipLimitAverage)) + showTipLimitWarning = true + } else if (inputTipLimit < perGasTipLimitAverage) { + errorMsg = appendError(errorMsg, qsTr("The average miner tip is %1 Gwei").arg(perGasTipLimitAverage), true) + } + + // Per-gas overall limit rules + if(inputOverallLimit < latestBaseFeeGwei){ + errorMsg = appendError(errorMsg, qsTr("The limit is below the current base fee of %1 %2").arg(latestBaseFeeGwei).arg("Gwei")) + showPriceLimitWarning = true + } + + /* TODO: change these values false once EIP1559 suggestions are revised + else if((inputOverallLimit - inputTipLimit) < latestBaseFeeGwei){ + errorMsg = appendError(errorMsg, qsTr("The limit should be at least %1 Gwei above the base fee").arg(perGasTipLimitFloor)) + } else if((inputOverallLimit - perGasTipLimitAverage) < latestBaseFeeGwei) { + errorMsg = appendError(errorMsg, qsTr("The maximum miner tip after the current base fee will be %1 Gwei, the minimum miner tip is currently %2 Gwei").arg(inputOverallLimit).arg(perGasTipLimitFloor), true) + showTipLimitWarning = true + }*/ + + errorsText.text = `${errorMsg}` + + } + + Component.onCompleted: { + updateGasEthValue() + checkLimits() + } function validate() { // causes error on application load without a null check - if (!inputGasLimit || !inputGasPrice) { + if (!inputGasLimit || !inputGasPrice || !inputPerGasTipLimit) { return } + inputGasLimit.validationError = "" inputGasPrice.validationError = "" + inputPerGasTipLimit.validationError = "" + const noInputLimit = inputGasLimit.text === "" const noInputPrice = inputGasPrice.text === "" + const noPerGasTip = inputPerGasTipLimit.text === "" + if (noInputLimit) { inputGasLimit.validationError = root.noInputErrorMessage } + if (noInputPrice) { inputGasPrice.validationError = root.noInputErrorMessage } + + if (noPerGasTip) { + inputPerGasTipLimit.validationError = root.noInputErrorMessage + } + if (isNaN(inputGasLimit.text)) { inputGasLimit.validationError = invalidInputErrorMessage } if (isNaN(inputGasPrice.text)) { inputGasPrice.validationError = invalidInputErrorMessage } + + if (isNaN(inputPerGasTipLimit.text)) { + inputPerGasTipLimit.validationError = invalidInputErrorMessage + } + let inputLimit = parseFloat(inputGasLimit.text || "0.00") let inputPrice = parseFloat(inputGasPrice.text || "0.00") - if (inputLimit <= 0) { + let inputTipLimit = parseFloat(inputPerGasTipLimit.text || "0.00") + + if (inputLimit <= 0.00) { inputGasLimit.validationError = root.greaterThan0ErrorMessage } - if (inputPrice <= 0) { + + if (inputPrice <= 0.00) { inputGasPrice.validationError = root.greaterThan0ErrorMessage } - const isValid = inputGasLimit.validationError === "" && inputGasPrice.validationError === "" - return isValid + if (inputTipLimit <= 0.00) { + inputPerGasTipLimit.validationError = root.greaterThan0ErrorMessage + } + const isInputValid = inputGasLimit.validationError === "" && inputGasPrice.validationError === "" && inputPerGasTipLimit.validationError === "" + return isInputValid } @@ -91,7 +195,20 @@ Item { color: Style.current.textColor } + StyledText { + id: baseFeeText + visible: eip1599Enabled && advancedMode + anchors.top: parent.top + anchors.left: prioritytext.right + anchors.leftMargin: Style.current.smallPadding + text: qsTr("Current base fee: %1 %2").arg(latestBaseFeeGwei).arg("Gwei") + font.weight: Font.Medium + font.pixelSize: 13 + color: Style.current.secondaryText + } + StatusButton { + visible: false // Change to TRUE once EIP1559 suggestions are revised id: buttonAdvanced anchors.verticalCenter: prioritytext.verticalCenter anchors.right: parent.right @@ -119,14 +236,25 @@ Item { GasSelectorButton { buttonGroup: gasGroup - //% "Low" - text: qsTrId("low") - price: slowestGasPrice + text: qsTr("Low") + price: { + if (!eip1599Enabled) return gasPrice; + return formatDec(suggestedFees.maxFeePerGasL, 6) + } gasLimit: inputGasLimit ? inputGasLimit.text : "" getGasEthValue: root.getGasEthValue getFiatValue: root.getFiatValue defaultCurrency: root.defaultCurrency - onChecked: inputGasPrice.text = price + onChecked: { + if (eip1599Enabled){ + inputPerGasTipLimit.text = formatDec(suggestedFees.maxPriorityFeePerGas, 2); + inputGasPrice.text = formatDec(suggestedFees.maxFeePerGasL, 2); + } else { + inputGasPrice.text = price + } + root.updateGasEthValue() + root.checkLimits() + } } GasSelectorButton { id: optimalGasButton @@ -135,28 +263,52 @@ Item { //% "Optimal" text: qsTrId("optimal") price: { - const price = (fastestGasPrice + slowestGasPrice) / 2 - // Setting the gas price field here because the binding didn't work - inputGasPrice.text = price - return price + if (!eip1599Enabled) { + const price = gasPrice + // Setting the gas price field here because the binding didn't work + inputGasPrice.text = price + return price + } + + return formatDec(suggestedFees.maxFeePerGasM, 6) } gasLimit: inputGasLimit ? inputGasLimit.text : "" getGasEthValue: root.getGasEthValue getFiatValue: root.getFiatValue defaultCurrency: root.defaultCurrency - onChecked: inputGasPrice.text = price + onChecked: { + if (eip1599Enabled){ + inputPerGasTipLimit.text = formatDec(suggestedFees.maxPriorityFeePerGas, 2); + inputGasPrice.text = formatDec(suggestedFees.maxFeePerGasM, 2); + } else { + inputGasPrice.text = price + } + root.updateGasEthValue() + root.checkLimits() + } } GasSelectorButton { buttonGroup: gasGroup - //% "High" - text: qsTrId("high") - price: fastestGasPrice + text: qsTr("High") + price: { + if (!eip1599Enabled) return gasPrice; + return formatDec(suggestedFees.maxFeePerGasH,6); + } gasLimit: inputGasLimit ? inputGasLimit.text : "" getGasEthValue: root.getGasEthValue getFiatValue: root.getFiatValue defaultCurrency: root.defaultCurrency - onChecked: inputGasPrice.text = price + onChecked: { + if (eip1599Enabled){ + inputPerGasTipLimit.text = formatDec(suggestedFees.maxPriorityFeePerGas, 2); + inputGasPrice.text = formatDec(suggestedFees.maxFeePerGasH, 2); + } else { + inputGasPrice.text = price + } + root.updateGasEthValue() + root.checkLimits() + } } } @@ -173,10 +325,11 @@ Item { //% "Gas amount limit" label: qsTrId("gas-amount-limit") text: "21000" + inputLabel.color: Style.current.secondaryText customHeight: 56 anchors.top: parent.top anchors.left: parent.left - anchors.right: inputGasPrice.left + anchors.right: eip1599Enabled ? inputPerGasTipLimit.left : inputGasPrice.left anchors.rightMargin: Style.current.padding placeholderText: "21000" validator: IntValidator{ @@ -187,23 +340,59 @@ Item { onTextChanged: { if (root.validate()) { root.updateGasEthValue() + root.checkLimits() } } } + Input { + id: inputPerGasTipLimit + label: qsTr("Per-gas tip limit") + inputLabel.color: Style.current.secondaryText + anchors.top: parent.top + anchors.right: inputGasPrice.left + anchors.rightMargin: Style.current.padding + anchors.left: undefined + visible: eip1599Enabled + width: 125 + customHeight: 56 + text: formatDec(suggestedFees.maxPriorityFeePerGas, 2); + placeholderText: "20" + onTextChanged: { + if (root.validate()) { + root.updateGasEthValue() + root.checkLimits() + } + } + } + + StyledText { + color: Style.current.secondaryText + //% "Gwei" + text: qsTrId("gwei") + visible: eip1599Enabled + anchors.top: parent.top + anchors.topMargin: 42 + anchors.right: inputPerGasTipLimit.right + anchors.rightMargin: Style.current.padding + font.pixelSize: 15 + } + Input { id: inputGasPrice //% "Per-gas overall limit" label: qsTrId("per-gas-overall-limit") + inputLabel.color: Style.current.secondaryText anchors.top: parent.top anchors.left: undefined anchors.right: parent.right - width: 130 + width: 125 customHeight: 56 placeholderText: "20" onTextChanged: { if (root.validate()) { root.updateGasEthValue() + root.checkLimits() } } } @@ -220,17 +409,37 @@ Item { } StyledText { - id: maxPriorityFeeText - //% "Maximum priority fee: %1 ETH" - text: qsTrId("maximum-priority-fee---1-eth").arg(selectedGasEthValue) + id: errorsText + text: "" + width: parent.width - Style.current.padding + visible: text != "" + height: visible ? undefined : 0 anchors.top: inputGasLimit.bottom - anchors.topMargin: 19 + anchors.topMargin: Style.current.smallPadding + 5 font.pixelSize: 13 + textFormat: Text.RichText + color: Style.current.secondaryText + wrapMode: Text.WordWrap + } + + StyledText { + id: maxPriorityFeeText + anchors.left: parent.left + //% "Maximum priority fee: %1 ETH" + text: { + let v = selectedGasEthValue > 0.00009 ? selectedGasEthValue : + (selectedGasEthValue < 0.000001 ? "0.000000..." : selectedGasEthValue.toFixed(6)) + return qsTrId("maximum-priority-fee---1-eth").arg(v) + } + anchors.top: errorsText.bottom + anchors.topMargin: Style.current.smallPadding + 5 + font.pixelSize: 13 + color: Style.current.textColor } StyledText { id: maxPriorityFeeFiatText - text: `${selectedGasFiatValue} ${root.defaultCurrency}` + text: `${selectedGasFiatValue} ${root.defaultCurrency.toUpperCase()}` anchors.verticalCenter: maxPriorityFeeText.verticalCenter anchors.left: maxPriorityFeeText.right anchors.leftMargin: 6 diff --git a/ui/shared/GasSelectorButton.qml b/ui/shared/GasSelectorButton.qml index 2d8ecf946a..99618f070b 100644 --- a/ui/shared/GasSelectorButton.qml +++ b/ui/shared/GasSelectorButton.qml @@ -17,11 +17,17 @@ Rectangle { property bool checkedByDefault: false property var getGasEthValue: function () {} property var getFiatValue: function () {} + + + function formatDec(num, dec){ + return Math.round((num + Number.EPSILON) * Math.pow(10, dec)) / Math.pow(10, dec) + } + property double ethValue: { if (!gasLimit) { return 0 } - return getGasEthValue(price, gasLimit) + return formatDec(parseFloat(getGasEthValue(price, gasLimit)), 6) } property double fiatValue: getFiatValue(ethValue, "ETH", defaultCurrency) signal checked() diff --git a/ui/shared/Input.qml b/ui/shared/Input.qml index 8833d08755..bad3f154e3 100644 --- a/ui/shared/Input.qml +++ b/ui/shared/Input.qml @@ -9,6 +9,8 @@ import "." Item { property alias textField: inputValue + property alias inputLabel: inputLabel + property string placeholderText: "My placeholder" property string placeholderTextColor: Style.current.secondaryText property alias text: inputValue.text diff --git a/ui/shared/status/StatusETHTransactionModal.qml b/ui/shared/status/StatusETHTransactionModal.qml index 45b17b5b31..e32bfb1ee9 100644 --- a/ui/shared/status/StatusETHTransactionModal.qml +++ b/ui/shared/status/StatusETHTransactionModal.qml @@ -5,6 +5,7 @@ import QtQuick.Dialogs 1.3 import "../../imports" import "../../shared" import "../../shared/status" +import "../../app/AppLayouts/Wallet/" ModalPopup { id: root @@ -20,14 +21,17 @@ ModalPopup { walletModel.gasView.getGasPricePredictions() } + height: 540 function sendTransaction() { try { - let responseStr = onSendTransaction(selectFromAccount.selectedAccount.address, - gasSelector.selectedGasLimit, - gasSelector.selectedGasPrice, - transactionSigner.enteredPassword); - + let responseStr = profileModel.ens.setPubKey(root.ensUsername, + selectFromAccount.selectedAccount.address, + gasSelector.selectedGasLimit, + gasSelector.eip1599Enabled ? "" : gasSelector.selectedGasPrice, + gasSelector.selectedTipLimit, + gasSelector.selectedOverallLimit, + transactionSigner.enteredPassword) let response = JSON.parse(responseStr) if (!response.success) { @@ -105,11 +109,11 @@ ModalPopup { visible: true anchors.top: selectFromAccount.bottom anchors.topMargin: Style.current.bigPadding * 2 - slowestGasPrice: parseFloat(walletModel.gasView.safeLowGasPrice) - fastestGasPrice: parseFloat(walletModel.gasView.fastestGasPrice) + gasPrice: parseFloat(walletModel.gasView.gasPrice) getGasEthValue: walletModel.gasView.getGasEthValue getFiatValue: walletModel.balanceView.getFiatValue defaultCurrency: walletModel.balanceView.defaultCurrency + property var estimateGas: Backpressure.debounce(gasSelector, 600, function() { let estimatedGas = root.estimateGasFunction(selectFromAccount.selectedAccount); gasSelector.selectedGasLimit = estimatedGas @@ -168,7 +172,7 @@ ModalPopup { width: parent.width height: btnNext.height - StatusRoundButton { + StatusRoundButton { id: btnBack anchors.left: parent.left icon.name: "arrow-right" @@ -185,6 +189,13 @@ ModalPopup { } } + Component { + id: transactionSettingsConfirmationPopupComponent + TransactionSettingsConfirmationPopup { + + } + } + StatusButton { id: btnNext anchors.right: parent.right @@ -197,6 +208,27 @@ ModalPopup { if (stack.isLastGroup) { return root.sendTransaction() } + + if(gasSelector.eip1599Enabled && stack.currentGroup === group2 && gasSelector.advancedMode){ + if(gasSelector.showPriceLimitWarning || gasSelector.showTipLimitWarning){ + openPopup(transactionSettingsConfirmationPopupComponent, { + currentBaseFee: gasSelector.latestBaseFeeGwei, + currentMinimumTip: gasSelector.perGasTipLimitFloor, + currentAverageTip: gasSelector.perGasTipLimitAverage, + tipLimit: gasSelector.selectedTipLimit, + suggestedTipLimit: gasSelector.perGasTipLimitFloor, + priceLimit: gasSelector.selectedOverallLimit, + suggestedPriceLimit: gasSelector.latestBaseFeeGwei + gasSelector.perGasTipLimitFloor, + showPriceLimitWarning: gasSelector.showPriceLimitWarning, + showTipLimitWarning: gasSelector.showTipLimitWarning, + onConfirm: function(){ + stack.next(); + } + }) + return + } + } + stack.next() } } diff --git a/ui/shared/status/StatusSNTTransactionModal.qml b/ui/shared/status/StatusSNTTransactionModal.qml index ee270eed24..3ac3f68ebd 100644 --- a/ui/shared/status/StatusSNTTransactionModal.qml +++ b/ui/shared/status/StatusSNTTransactionModal.qml @@ -5,6 +5,7 @@ import QtQuick.Dialogs 1.3 import "../../imports" import "../../shared" import "../../shared/status" +import "../../app/AppLayouts/Wallet/" ModalPopup { id: root @@ -12,13 +13,15 @@ ModalPopup { property string assetPrice property string contractAddress property var estimateGasFunction: (function(userAddress, uuid) { return 0; }) - property var onSendTransaction: (function(userAddress, gasLimit, gasPrice, password){ return ""; }) + property var onSendTransaction: (function(userAddress, gasLimit, gasPrice, tipLimit, overallLimit, password){ return ""; }) property var onSuccess: (function(){}) Component.onCompleted: { - walletModel.gasView.getGasPricePredictions() + walletModel.gasView.getGasPrice() } + height: 540 + //% "Authorize %1 %2" title: qsTrId("authorize--1--2").arg(Utils.stripTrailingZeros(assetPrice)).arg(asset.symbol) @@ -33,13 +36,16 @@ ModalPopup { function setAsyncGasLimitResult(uuid, value) { if (uuid === gasSelector.uuid) { gasSelector.selectedGasLimit = value + gasSelector.defaultGasLimit = value } } function sendTransaction() { let responseStr = onSendTransaction(selectFromAccount.selectedAccount.address, gasSelector.selectedGasLimit, - gasSelector.selectedGasPrice, + gasSelector.eip1599Enabled ? "" : gasSelector.selectedGasPrice, + gasSelector.selectedTipLimit, + gasSelector.selectedOverallLimit, transactionSigner.enteredPassword); let response = JSON.parse(responseStr) @@ -108,12 +114,12 @@ ModalPopup { id: gasSelector anchors.top: selectFromAccount.bottom anchors.topMargin: Style.current.bigPadding * 2 - slowestGasPrice: parseFloat(walletModel.gasView.safeLowGasPrice) - fastestGasPrice: parseFloat(walletModel.gasView.fastestGasPrice) + gasPrice: parseFloat(walletModel.gasView.gasPrice) getGasEthValue: walletModel.gasView.getGasEthValue getFiatValue: walletModel.balanceView.getFiatValue defaultCurrency: walletModel.balanceView.defaultCurrency width: stack.width + property var estimateGas: Backpressure.debounce(gasSelector, 600, function() { let estimatedGas = root.estimateGasFunction(selectFromAccount.selectedAccount, uuid); gasSelector.selectedGasLimit = estimatedGas @@ -190,6 +196,14 @@ ModalPopup { stack.back() } } + + Component { + id: transactionSettingsConfirmationPopupComponent + TransactionSettingsConfirmationPopup { + + } + } + StatusButton { id: btnNext anchors.right: parent.right @@ -203,6 +217,27 @@ ModalPopup { if (stack.isLastGroup) { return root.sendTransaction() } + + if(gasSelector.eip1599Enabled && stack.currentGroup === group2 && gasSelector.advancedMode){ + if(gasSelector.showPriceLimitWarning || gasSelector.showTipLimitWarning){ + openPopup(transactionSettingsConfirmationPopupComponent, { + currentBaseFee: gasSelector.latestBaseFeeGwei, + currentMinimumTip: gasSelector.perGasTipLimitFloor, + currentAverageTip: gasSelector.perGasTipLimitAverage, + tipLimit: gasSelector.selectedTipLimit, + suggestedTipLimit: gasSelector.perGasTipLimitFloor, // TODO: + priceLimit: gasSelector.selectedOverallLimit, + suggestedPriceLimit: gasSelector.latestBaseFeeGwei + gasSelector.perGasTipLimitFloor, + showPriceLimitWarning: gasSelector.showPriceLimitWarning, + showTipLimitWarning: gasSelector.showTipLimitWarning, + onConfirm: function(){ + stack.next(); + } + }) + return + } + } + stack.next() } } diff --git a/ui/shared/status/StatusStickerMarket.qml b/ui/shared/status/StatusStickerMarket.qml index 87162e496f..5adab84031 100644 --- a/ui/shared/status/StatusStickerMarket.qml +++ b/ui/shared/status/StatusStickerMarket.qml @@ -98,12 +98,14 @@ Item { if (packId < 0 || !selectedAccount || !price) return 325000 return chatsModel.stickers.estimate(packId, selectedAccount.address, price, uuid) } - onSendTransaction: function(selectedAddress, gasLimit, gasPrice, password) { + onSendTransaction: function(selectedAddress, gasLimit, gasPrice, tipLimit, overallLimit, password) { return chatsModel.stickers.buy(packId, selectedAddress, price, gasLimit, gasPrice, + tipLimit, + overallLimit, password) } onClosed: { diff --git a/ui/shared/status/StatusStickerPackClickPopup.qml b/ui/shared/status/StatusStickerPackClickPopup.qml index 4f9350f930..248c5dcf00 100644 --- a/ui/shared/status/StatusStickerPackClickPopup.qml +++ b/ui/shared/status/StatusStickerPackClickPopup.qml @@ -60,12 +60,14 @@ ModalPopup { if (packId < 0 || !selectedAccount || !price) return 325000 return chatsModel.stickers.estimate(packId, selectedAccount.address, price, uuid) } - onSendTransaction: function(selectedAddress, gasLimit, gasPrice, password) { + onSendTransaction: function(selectedAddress, gasLimit, gasPrice, tipLimit, overallLimit, password) { return chatsModel.stickers.buy(packId, selectedAddress, price, gasLimit, gasPrice, + tipLimit, + overallLimit, password) } onClosed: { diff --git a/vendor/status-lib b/vendor/status-lib index efe2790db6..c1d61a13c0 160000 --- a/vendor/status-lib +++ b/vendor/status-lib @@ -1 +1 @@ -Subproject commit efe2790db6cf5e3f01d4b3265d2a671fed70e2d1 +Subproject commit c1d61a13c0592e83083161e4b6d5b2e126a8a424