diff --git a/src/app/wallet/view.nim b/src/app/wallet/view.nim index 18d98f2892..091d2d0387 100644 --- a/src/app/wallet/view.nim +++ b/src/app/wallet/view.nim @@ -1,7 +1,8 @@ -import NimQml, Tables, strformat, strutils, chronicles, json +import NimQml, Tables, strformat, strutils, chronicles, json, std/wrapnils, parseUtils, stint import ../../status/[status, wallet, threads] import ../../status/wallet/collectibles as status_collectibles import ../../status/libstatus/wallet as status_wallet +import ../../status/libstatus/utils import views/[asset_list, account_list, account_item, transaction_list, collectibles_list] QtObject: @@ -15,6 +16,11 @@ QtObject: status: Status totalFiatBalance: string etherscanLink: string + safeLowGasPrice: string + standardGasPrice: string + fastGasPrice: string + fastestGasPrice: string + defaultGasLimit: string proc delete(self: WalletView) = self.accounts.delete @@ -36,6 +42,11 @@ QtObject: result.currentCollectiblesList = newCollectiblesList() result.totalFiatBalance = "" result.etherscanLink = "" + result.safeLowGasPrice = "0" + result.standardGasPrice = "0" + result.fastGasPrice = "0" + result.fastestGasPrice = "0" + result.defaultGasLimit = "22000" result.setup proc etherscanLinkChanged*(self: WalletView) {.signal.} @@ -156,6 +167,17 @@ QtObject: proc getCryptoValue*(self: WalletView, fiatBalance: string, fiatSymbol: string, cryptoSymbol: string): string {.slot.} = result = fmt"{self.status.wallet.convertValue(fiatBalance, fiatSymbol, cryptoSymbol)}" + proc getGasEthValue*(self: WalletView, gweiValue: string, gasLimit: string): string {.slot.} = + var gweiValueInt:int + var gasLimitInt:int + + discard gweiValue.parseInt(gweiValueInt) + discard gasLimit.parseInt(gasLimitInt) + + let weiValue = gweiValueInt.u256 * 1000000000.u256 * gasLimitInt.u256 + let ethValue = wei2Eth(weiValue) + result = fmt"{ethValue}" + proc generateNewAccount*(self: WalletView, password: string, accountName: string, color: string): string {.slot.} = result = self.status.wallet.generateNewAccount(password, accountName, color) @@ -271,3 +293,38 @@ QtObject: if address == self.currentAccount.address: self.setCurrentTransactions(transactions) self.loadingTrxHistory(false) + + proc gasPricePredictionsChanged*(self: WalletView) {.signal.} + + proc getGasPricePredictions*(self: WalletView) {.slot.} = + let prediction = self.status.wallet.getGasPricePredictions() + self.safeLowGasPrice = prediction.safeLow + self.standardGasPrice = prediction.standard + self.fastGasPrice = prediction.fast + self.fastestGasPrice = prediction.fastest + self.gasPricePredictionsChanged() + + proc safeLowGasPrice*(self: WalletView): string {.slot.} = result = ?.self.safeLowGasPrice + QtProperty[string] safeLowGasPrice: + read = safeLowGasPrice + notify = gasPricePredictionsChanged + + proc standardGasPrice*(self: WalletView): string {.slot.} = result = ?.self.standardGasPrice + QtProperty[string] standardGasPrice: + read = standardGasPrice + notify = gasPricePredictionsChanged + + proc fastGasPrice*(self: WalletView): string {.slot.} = result = ?.self.fastGasPrice + QtProperty[string] fastGasPrice: + read = fastGasPrice + notify = gasPricePredictionsChanged + + proc fastestGasPrice*(self: WalletView): string {.slot.} = result = ?.self.fastestGasPrice + QtProperty[string] fastestGasPrice: + read = fastestGasPrice + notify = gasPricePredictionsChanged + + proc defaultGasLimit*(self: WalletView): string {.slot.} = result = ?.self.defaultGasLimit + QtProperty[string] defaultGasLimit: + read = defaultGasLimit + diff --git a/src/status/libstatus/types.nim b/src/status/libstatus/types.nim index fed5735f59..0e37a515e3 100644 --- a/src/status/libstatus/types.nim +++ b/src/status/libstatus/types.nim @@ -21,6 +21,12 @@ type SignalType* {.pure.} = enum WhisperFilterAdded = "whisper.filter.added" Unknown +type GasPricePrediction* = object + safeLow*: string + standard*: string + fast*: string + fastest*: string + type DerivedAccount* = object publicKey*: string address*: string @@ -166,4 +172,4 @@ type id*: string name*: string etherscanLink* {.serializedFieldName("etherscan-link").}: string - config*: NodeConfig \ No newline at end of file + config*: NodeConfig diff --git a/src/status/wallet.nim b/src/status/wallet.nim index 722458caa5..5aa1e32dd0 100644 --- a/src/status/wallet.nim +++ b/src/status/wallet.nim @@ -1,4 +1,4 @@ -import eventemitter, json, strformat, strutils, chronicles, sequtils +import eventemitter, json, strformat, strutils, chronicles, sequtils, httpclient import json_serialization from eth/common/utils import parseAddress import libstatus/accounts as status_accounts @@ -6,7 +6,7 @@ import libstatus/tokens as status_tokens import libstatus/settings as status_settings import libstatus/wallet as status_wallet import libstatus/accounts/constants as constants -from libstatus/types import GeneratedAccount, DerivedAccount, Transaction, Setting +from libstatus/types import GeneratedAccount, DerivedAccount, Transaction, Setting, GasPricePrediction import wallet/balance_manager import wallet/account import wallet/collectibles @@ -187,3 +187,15 @@ proc validateMnemonic*(self: WalletModel, mnemonic: string): string = result = status_wallet.validateMnemonic(mnemonic).parseJSON()["error"].getStr proc getAllCollectibles*(self: WalletModel, address: string): seq[Collectible] = getAllCollectibles(address) + +proc getGasPricePredictions*(self: WalletModel): GasPricePrediction = + try: + let url: string = fmt"https://etherchain.org/api/gasPriceOracle" + let client = newHttpClient() + client.headers = newHttpHeaders({ "Content-Type": "application/json" }) + let response = client.request(url) + result = Json.decode(response.body, GasPricePrediction) + except Exception as e: + echo "error getting gas price predictions" + echo e.msg + diff --git a/ui/app/AppLayouts/Wallet/WalletHeader.qml b/ui/app/AppLayouts/Wallet/WalletHeader.qml index 2c0026b5f7..70aa0254c1 100644 --- a/ui/app/AppLayouts/Wallet/WalletHeader.qml +++ b/ui/app/AppLayouts/Wallet/WalletHeader.qml @@ -69,6 +69,9 @@ Item { SendModal{ id: sendModal + onOpened: { + walletModel.getGasPricePredictions() + } } ReceiveModal{ diff --git a/ui/app/AppLayouts/Wallet/components/SendModalContent.qml b/ui/app/AppLayouts/Wallet/components/SendModalContent.qml index 53305cecbb..6cc2f32f64 100644 --- a/ui/app/AppLayouts/Wallet/components/SendModalContent.qml +++ b/ui/app/AppLayouts/Wallet/components/SendModalContent.qml @@ -92,12 +92,23 @@ Item { } } + GasSelector { + id: gasSelector + anchors.top: selectFromAccount.bottom + anchors.topMargin: Style.current.bigPadding + slowestGasPrice: parseFloat(walletModel.safeLowGasPrice) + fastestGasPrice: parseFloat(walletModel.fastestGasPrice) + getGasEthValue: walletModel.getGasEthValue + getFiatValue: walletModel.getFiatValue + defaultCurrency: walletModel.defaultCurrency + } + RecipientSelector { id: selectRecipient accounts: walletModel.accounts contacts: profileModel.addedContacts label: qsTr("Recipient") - anchors.top: selectFromAccount.bottom + anchors.top: gasSelector.bottom anchors.topMargin: Style.current.padding anchors.left: parent.left anchors.right: parent.right diff --git a/ui/shared/GasSelector.qml b/ui/shared/GasSelector.qml new file mode 100644 index 0000000000..41bea01f85 --- /dev/null +++ b/ui/shared/GasSelector.qml @@ -0,0 +1,205 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 +import "../imports" +import "./" + +Item { + id: root + anchors.left: parent.left + anchors.right: parent.right + height: sliderWrapper.height + Style.current.smallPadding + txtNetworkFee.height + buttonAdvanced.height + property double slowestGasPrice: 0 + property double fastestGasPrice: 100 + property double stepSize: ((root.fastestGasPrice - root.slowestGasPrice) / 10).toFixed(1) + property var getGasEthValue: function () {} + property var getFiatValue: function () {} + property string defaultCurrency: "USD" + property alias selectedGasPrice: inputGasPrice.text + property alias selectedGasLimit: inputGasLimit.text + + function defaultGasPrice() { + return ((50 * (root.fastestGasPrice - root.slowestGasPrice) / 100) + root.slowestGasPrice) + } + + function updateGasEthValue() { + let ethValue = root.getGasEthValue(inputGasPrice.text, inputGasLimit.text) + let fiatValue = root.getFiatValue(ethValue, "ETH", root.defaultCurrency) + let summary = ethValue + " ETH ~" + fiatValue + " " + root.defaultCurrency.toUpperCase() + labelGasPriceSummary.text = summary + labelGasPriceSummaryAdvanced.text = summary + } + + StyledText { + id: txtNetworkFee + anchors.top: parent.top + anchors.left: parent.left + text: qsTr("Network fee") + font.weight: Font.Medium + font.pixelSize: 13 + color: Style.current.textColor + } + + StyledText { + id: labelGasPriceSummary + anchors.top: parent.top + anchors.right: parent.right + font.weight: Font.Medium + font.pixelSize: 13 + color: Style.current.secondaryText + } + + Item { + id: sliderWrapper + anchors.topMargin: Style.current.smallPadding + anchors.top: labelGasPriceSummary.bottom + height: sliderWrapper.visible ? gasSlider.height + labelSlow.height + Style.current.padding : 0 + width: parent.width + visible: Number(root.selectedGasPrice) >= Number(root.slowestGasPrice) && Number(root.selectedGasPrice) <= Number(root.fastestGasPrice) + + StatusSlider { + id: gasSlider + minimumValue: root.slowestGasPrice + maximumValue: root.fastestGasPrice + stepSize: root.stepSize + value: root.defaultGasPrice() + onValueChanged: { + if (!isNaN(gasSlider.value)) { + inputGasPrice.text = gasSlider.value + "" + root.updateGasEthValue() + } + } + visible: parent.visible + } + + StyledText { + id: labelSlow + anchors.top: gasSlider.bottom + anchors.topMargin: Style.current.padding + anchors.left: parent.left + text: qsTr("Slow") + font.pixelSize: 15 + color: Style.current.textColor + visible: parent.visible + } + + StyledText { + id: labelOptimal + anchors.top: gasSlider.bottom + anchors.topMargin: Style.current.padding + anchors.horizontalCenter: gasSlider.horizontalCenter + text: qsTr("Optimal") + font.pixelSize: 15 + color: Style.current.textColor + visible: parent.visible + } + + StyledText { + id: labelFast + anchors.top: gasSlider.bottom + anchors.topMargin: Style.current.padding + anchors.right: parent.right + text: qsTr("Fast") + font.pixelSize: 15 + color: Style.current.textColor + visible: parent.visible + } + } + + StyledButton { + id: buttonReset + anchors.top: sliderWrapper.bottom + anchors.topMargin: sliderWrapper.visible ? Style.current.smallPadding : 0 + anchors.right: buttonAdvanced.left + anchors.rightMargin: -Style.current.padding + label: qsTr("Reset") + btnColor: "transparent" + textSize: 13 + visible: !sliderWrapper.visible + onClicked: { + gasSlider.value = root.defaultGasPrice() + inputGasPrice.text = root.defaultGasPrice() + } + } + + StyledButton { + id: buttonAdvanced + anchors.top: sliderWrapper.bottom + anchors.topMargin: sliderWrapper.visible ? Style.current.smallPadding : 0 + anchors.right: parent.right + anchors.rightMargin: -Style.current.padding + label: qsTr("Advanced") + btnColor: "transparent" + textSize: 13 + onClicked: { + customNetworkFeeDialog.open() + } + } + + ModalPopup { + id: customNetworkFeeDialog + title: qsTr("Custom Network Fee") + height: 386 + + Input { + id: inputGasLimit + label: qsTr("Gas limit") + text: "22000" + customHeight: 56 + anchors.top: parent.top + onTextChanged: { + if (inputGasLimit.text.trim() === "") { + inputGasLimit.text = root.selectedGasLimit + } + root.updateGasEthValue() + } + } + + Input { + id: inputGasPrice + label: qsTr("Gas price") + anchors.top: inputGasLimit.bottom + anchors.topMargin: Style.current.smallPadding + customHeight: 56 + text: root.defaultGasPrice() + onTextChanged: { + if (inputGasPrice.text.trim() === "") { + inputGasPrice.text = root.defaultGasPrice() + } + root.updateGasEthValue() + } + + StyledText { + color: Style.current.darkGrey + text: qsTr("Gwei") + anchors.top: parent.top + anchors.topMargin: 42 + anchors.right: parent.right + anchors.rightMargin: Style.current.padding + font.pixelSize: 15 + } + } + + StyledText { + id: labelGasPriceSummaryAdvanced + anchors.top: inputGasPrice.bottom + anchors.topMargin: Style.current.smallPadding + anchors.right: parent.right + font.weight: Font.Medium + font.pixelSize: 13 + color: Style.current.secondaryText + } + + footer: StyledButton { + id: applyButton + anchors.right: parent.right + anchors.rightMargin: Style.current.smallPadding + label: qsTr("Apply") + anchors.bottom: parent.bottom + onClicked: { + root.updateGasEthValue() + customNetworkFeeDialog.close() + } + } + } +} diff --git a/ui/shared/StatusSlider.qml b/ui/shared/StatusSlider.qml index e2810997e2..1073b7fe77 100644 --- a/ui/shared/StatusSlider.qml +++ b/ui/shared/StatusSlider.qml @@ -8,7 +8,6 @@ QQC1.Slider { id: slider anchors.left: parent.left anchors.right: parent.right - stepSize: ((slider.maximumValue - slider.minimumValue) / 10).toFixed(1) style: SliderStyle { groove: Rectangle { implicitHeight: 4