diff --git a/src/app/global/utils.nim b/src/app/global/utils.nim index 5d97129883..f28b2c8689 100644 --- a/src/app/global/utils.nim +++ b/src/app/global/utils.nim @@ -61,6 +61,9 @@ QtObject: proc hex2Eth*(self: Utils, value: string): string {.slot.} = return stripTrailingZeroes(conversion.wei2Eth(stint.fromHex(StUint[256], value))) + proc hex2Gwei*(self: Utils, value: string): string {.slot.} = + return stripTrailingZeroes(conversion.wei2Eth(stint.fromHex(StUint[256], value)*1000000000)) + proc gwei2Hex*(self: Utils, gwei: float): string {.slot.} = return "0x" & conversion.gwei2Wei(gwei).toHex() diff --git a/src/app/modules/main/wallet_section/transactions/controller.nim b/src/app/modules/main/wallet_section/transactions/controller.nim index 1d361af382..d7eb37e930 100644 --- a/src/app/modules/main/wallet_section/transactions/controller.nim +++ b/src/app/modules/main/wallet_section/transactions/controller.nim @@ -113,3 +113,6 @@ proc getChainIdForBrowser*(self: Controller): int = proc getEstimatedTime*(self: Controller, chainId: int, maxFeePerGas: string): EstimatedTime = return self.transactionService.getEstimatedTime(chainId, maxFeePerGas) + +proc getLastTxBlockNumber*(self: Controller): string = + return self.transactionService.getLastTxBlockNumber(self.networkService.getNetworkForBrowser().chainId) 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 420a16bcdb..a7150884db 100644 --- a/src/app/modules/main/wallet_section/transactions/io_interface.nim +++ b/src/app/modules/main/wallet_section/transactions/io_interface.nim @@ -72,3 +72,6 @@ method getEstimatedTime*(self: AccessInterface, chainId: int, maxFeePerGas: stri # inheritance, which is not well supported in Nim. method viewDidLoad*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") + +method getLastTxBlockNumber*(self: AccessInterface): string {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/wallet_section/transactions/item.nim b/src/app/modules/main/wallet_section/transactions/item.nim index 0305b3a1f1..f1b6413841 100644 --- a/src/app/modules/main/wallet_section/transactions/item.nim +++ b/src/app/modules/main/wallet_section/transactions/item.nim @@ -24,6 +24,9 @@ type txHash: string multiTransactionID: int isTimeStamp: bool + baseGasFees: string + totalFees: string + maxTotalFees: string proc initItem*( id: string, @@ -47,7 +50,10 @@ proc initItem*( input: string, txHash: string, multiTransactionID: int, - isTimeStamp: bool + isTimeStamp: bool, + baseGasFees: string, + totalFees: string, + maxTotalFees: string ): Item = result.id = id result.typ = typ @@ -71,6 +77,9 @@ proc initItem*( result.txHash = txHash result.multiTransactionID = multiTransactionID result.isTimeStamp = isTimeStamp + result.baseGasFees = baseGasFees + result.totalFees = totalFees + result.maxTotalFees = maxTotalFees proc `$`*(self: Item): string = result = fmt"""AllTokensItem( @@ -96,6 +105,9 @@ proc `$`*(self: Item): string = txHash: {self.txHash}, multiTransactionID: {self.multiTransactionID}, isTimeStamp: {self.isTimeStamp}, + baseGasFees: {self.baseGasFees}, + totalFees: {self.totalFees}, + maxTotalFees: {self.maxTotalFees}, ]""" proc getId*(self: Item): string = @@ -163,3 +175,12 @@ proc getMultiTransactionID*(self: Item): int = proc getIsTimeStamp*(self: Item): bool = return self.isTimeStamp + +proc getBaseGasFees*(self: Item): string = + return self.baseGasFees + +proc getTotalFees*(self: Item): string = + return self.totalFees + +proc getMaxTotalFees*(self: Item): string = + return self.maxTotalFees diff --git a/src/app/modules/main/wallet_section/transactions/model.nim b/src/app/modules/main/wallet_section/transactions/model.nim index 3b7617d5c0..ad93303985 100644 --- a/src/app/modules/main/wallet_section/transactions/model.nim +++ b/src/app/modules/main/wallet_section/transactions/model.nim @@ -1,4 +1,4 @@ -import NimQml, Tables, strutils, strformat, sequtils, tables, sugar, algorithm, std/[times, os], stint +import NimQml, Tables, strutils, strformat, sequtils, tables, sugar, algorithm, std/[times, os], stint, parseutils import ./item import ../../../../../app_service/service/eth/utils as eth_service_utils @@ -28,6 +28,9 @@ type TxHash MultiTransactionID IsTimeStamp + BaseGasFees + TotalFees + MaxTotalFees QtObject: type @@ -88,7 +91,10 @@ QtObject: ModelRole.Input.int:"input", ModelRole.TxHash.int:"txHash", ModelRole.MultiTransactionID.int:"multiTransactionID", - ModelRole.IsTimeStamp.int: "isTimeStamp" + ModelRole.IsTimeStamp.int: "isTimeStamp", + ModelRole.BaseGasFees.int: "baseGasFees", + ModelRole.TotalFees.int: "totalFees", + ModelRole.MaxTotalFees.int: "maxTotalFees" }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -145,7 +151,13 @@ QtObject: of ModelRole.MultiTransactionID: result = newQVariant(item.getMultiTransactionID()) of ModelRole.IsTimeStamp: - result = newQVariant(item.getIsTimeStamp()) + result = newQVariant(item.getIsTimeStamp()) + of ModelRole.BaseGasFees: + result = newQVariant(item.getBaseGasFees()) + of ModelRole.TotalFees: + result = newQVariant(item.getTotalFees()) + of ModelRole.MaxTotalFees: + result = newQVariant(item.getMaxTotalFees()) proc setItems*(self: Model, items: seq[Item]) = self.beginResetModel() @@ -206,7 +218,10 @@ QtObject: t.input, t.txHash, t.multiTransactionID, - false + false, + t.baseGasFees, + t.totalFees, + t.maxTotalFees, )) var allTxs = self.items.concat(newTxItems) @@ -219,7 +234,7 @@ QtObject: for tx in allTxs: let duration = fromUnix(tx.getTimestamp()) - tempTimeStamp if(duration.inDays != 0): - itemsWithDateHeaders.add(initItem("", "", "", "", "", tx.getTimestamp(), "", "", "", "", "", "", "", "", "", 0, "", "", "", "", 0, true)) + itemsWithDateHeaders.add(initItem("", "", "", "", "", tx.getTimestamp(), "", "", "", "", "", "", "", "", "", 0, "", "", "", "", 0, true, "", "", "")) itemsWithDateHeaders.add(tx) tempTimeStamp = fromUnix(tx.getTimestamp()) diff --git a/src/app/modules/main/wallet_section/transactions/module.nim b/src/app/modules/main/wallet_section/transactions/module.nim index 2038749e91..c68d1d27b4 100644 --- a/src/app/modules/main/wallet_section/transactions/module.nim +++ b/src/app/modules/main/wallet_section/transactions/module.nim @@ -113,3 +113,6 @@ method getChainIdForBrowser*(self: Module): int = method getEstimatedTime*(self: Module, chainId: int, maxFeePerGas: string): int = return self.controller.getEstimatedTime(chainId, maxFeePerGas).int + +method getLastTxBlockNumber*(self: Module): string = + return self.controller.getLastTxBlockNumber() diff --git a/src/app/modules/main/wallet_section/transactions/view.nim b/src/app/modules/main/wallet_section/transactions/view.nim index 3950f1aaa7..b96535ac81 100644 --- a/src/app/modules/main/wallet_section/transactions/view.nim +++ b/src/app/modules/main/wallet_section/transactions/view.nim @@ -150,3 +150,6 @@ QtObject: proc getEstimatedTime*(self: View, chainId: int, maxFeePerGas: string): int {.slot.} = return self.delegate.getEstimatedTime(chainId, maxFeePerGas) + + proc getLastTxBlockNumber*(self: View): string {.slot.} = + return self.delegate.getLastTxBlockNumber() diff --git a/src/app_service/service/transaction/dto.nim b/src/app_service/service/transaction/dto.nim index 1b8d2ffcac..37570f151b 100644 --- a/src/app_service/service/transaction/dto.nim +++ b/src/app_service/service/transaction/dto.nim @@ -49,6 +49,21 @@ type input*: string txHash*: string multiTransactionID*: int + baseGasFees*: string + totalFees*: string + maxTotalFees*: string + + +proc getTotalFees(tip: string, baseFee: string, gasUsed: string, maxFee: string): string = + var maxFees = stint.fromHex(Uint256, maxFee) + var totalGasUsed = stint.fromHex(Uint256, tip) + stint.fromHex(Uint256, baseFee) + if totalGasUsed > maxFees: + totalGasUsed = maxFees + var totalGasUsedInHex = (totalGasUsed * stint.fromHex(Uint256, gasUsed)).toHex + return totalGasUsedInHex + +proc getMaxTotalFees(maxFee: string, gasLimit: string): string = + return (stint.fromHex(Uint256, maxFee) * stint.fromHex(Uint256, gasLimit)).toHex proc toTransactionDto*(jsonObj: JsonNode): TransactionDto = result = TransactionDto() @@ -73,6 +88,9 @@ proc toTransactionDto*(jsonObj: JsonNode): TransactionDto = discard jsonObj.getProp("input", result.input) discard jsonObj.getProp("txHash", result.txHash) discard jsonObj.getProp("multiTransactionID", result.multiTransactionID) + discard jsonObj.getProp("base_gas_fee", result.baseGasFees) + result.totalFees = getTotalFees(result.maxPriorityFeePerGas, result.baseGasFees, result.gasUsed, result.maxFeePerGas) + result.maxTotalFees = getMaxTotalFees(result.maxFeePerGas, result.gasLimit) proc cmpTransactions*(x, y: TransactionDto): int = # Sort proc to compare transactions from a single account. diff --git a/src/app_service/service/transaction/service.nim b/src/app_service/service/transaction/service.nim index d513de5286..2f85505266 100644 --- a/src/app_service/service/transaction/service.nim +++ b/src/app_service/service/transaction/service.nim @@ -422,3 +422,11 @@ QtObject: except Exception as e: error "Error estimating transaction time", message = e.msg return EstimatedTime.Unknown + + proc getLastTxBlockNumber*(self: Service, chainId: int): string = + try: + let response = eth.getBlockByNumber(chainId, "latest") + return response.result{"number"}.getStr + except Exception as e: + error "Error getting latest block number", message = e.msg + return "" diff --git a/ui/StatusQ b/ui/StatusQ index 7108ac87cf..c3bbb396ec 160000 --- a/ui/StatusQ +++ b/ui/StatusQ @@ -1 +1 @@ -Subproject commit 7108ac87cffe529d011ec1c14473630e52980282 +Subproject commit c3bbb396ecd5ad25df565d64429aaba869df4b49 diff --git a/ui/app/AppLayouts/Wallet/WalletLayout.qml b/ui/app/AppLayouts/Wallet/WalletLayout.qml index 3bc7f189ba..237106a840 100644 --- a/ui/app/AppLayouts/Wallet/WalletLayout.qml +++ b/ui/app/AppLayouts/Wallet/WalletLayout.qml @@ -54,6 +54,7 @@ Item { id: walletContainer RightTabView { store: root.store + contactsStore: root.contactsStore sendModal: root.sendModal } } diff --git a/ui/app/AppLayouts/Wallet/views/RightTabView.qml b/ui/app/AppLayouts/Wallet/views/RightTabView.qml index 5a42aca4b5..5c8b5fe208 100644 --- a/ui/app/AppLayouts/Wallet/views/RightTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/RightTabView.qml @@ -6,6 +6,7 @@ import StatusQ.Controls 0.1 import utils 1.0 import shared.views 1.0 +import "./" import "../stores" import "../panels" import "../views/collectibles" @@ -15,6 +16,7 @@ Item { property alias currentTabIndex: walletTabBar.currentIndex property var store + property var contactsStore property var sendModal ColumnLayout { @@ -80,6 +82,10 @@ Item { } HistoryView { account: RootStore.currentAccount + onLaunchTransactionDetail: { + transactionDetailView.transaction = transaction + stack.currentIndex = 3 + } } } } @@ -94,6 +100,14 @@ Item { Layout.fillHeight: true onGoBack: stack.currentIndex = 0 } + TransactionDetailView { + id: transactionDetailView + Layout.fillWidth: true + Layout.fillHeight: true + sendModal: root.sendModal + contactsStore: root.contactsStore + onGoBack: stack.currentIndex = 0 + } } WalletFooter { diff --git a/ui/app/AppLayouts/Wallet/views/SavedAddressesView.qml b/ui/app/AppLayouts/Wallet/views/SavedAddressesView.qml index 12c405bb55..18fb49d469 100644 --- a/ui/app/AppLayouts/Wallet/views/SavedAddressesView.qml +++ b/ui/app/AppLayouts/Wallet/views/SavedAddressesView.qml @@ -26,6 +26,16 @@ Item { id: _internal property bool loading: false property string error: "" + function saveAddress(name, address) { + loading = true + error = RootStore.createOrUpdateSavedAddress(name, address) + loading = false + } + function deleteSavedAddress(address) { + loading = true + error = RootStore.deleteSavedAddress(address) + loading = false + } } Item { @@ -65,155 +75,6 @@ Item { } } - Component { - id: delegateSavedAddress - StatusListItem { - id: savedAddress - title: name - objectName: name - subTitle: name + " \u2022 " + Utils.getElidedCompressedPk(address) - implicitWidth: parent.width - color: "transparent" - border.color: Theme.palette.baseColor5 - //TODO uncomment when #6456 is fixed - //titleTextIcon: RootStore.favouriteAddress ? "star-icon" : "" - statusListItemComponentsSlot.spacing: 0 - property bool showButtons: sensor.containsMouse - - components: [ - StatusRoundButton { - icon.color: savedAddress.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1 - type: StatusRoundButton.Type.Tertiary - icon.name: "send" - onClicked: { - root.sendModal.open(address); - } - }, - CopyToClipBoardButton { - type: StatusRoundButton.Type.Tertiary - icon.color: savedAddress.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1 - store: RootStore - textToCopy: address - }, - //TODO uncomment when #6456 is fixed -// StatusRoundButton { -// icon.color: savedAddress.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1 -// type: StatusRoundButton.Type.Tertiary -// icon.name: savedAddress.favouriteAddress ? "favourite" : "unfavourite" -// onClicked: { -// RootStore.setFavourite(); -// } -// }, - StatusRoundButton { - icon.color: savedAddress.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1 - type: StatusRoundButton.Type.Tertiary - icon.name: "more" - onClicked: { - editDeleteMenu.openMenu(name, address); - } - } - ] - } - } - - StatusPopupMenu { - id: editDeleteMenu - property string contactName - property string contactAddress - function openMenu(name, address) { - contactName = name; - contactAddress = address; - popup(); - } - onClosed: { - contactName = ""; - contactAddress = ""; - } - StatusMenuItem { - text: qsTr("Edit") - objectName: "editSavedAddress" - icon.name: "pencil-outline" - onTriggered: { - Global.openPopup(addEditSavedAddress, - { - edit: true, - address: editDeleteMenu.contactAddress, - name: editDeleteMenu.contactName - }) - } - } - StatusMenuSeparator { } - StatusMenuItem { - text: qsTr("Delete") - type: StatusMenuItem.Type.Danger - icon.name: "delete" - objectName: "deleteSavedAddress" - onTriggered: { - deleteAddressConfirm.name = editDeleteMenu.contactName; - deleteAddressConfirm.address = editDeleteMenu.contactAddress; - deleteAddressConfirm.open() - } - } - } - - Component { - id: addEditSavedAddress - AddEditSavedAddressPopup { - id: addEditModal - anchors.centerIn: parent - onClosed: destroy() - contactsStore: root.contactsStore - onSave: { - _internal.loading = true - _internal.error = RootStore.createOrUpdateSavedAddress(name, address) - _internal.loading = false - close() - } - } - } - - StatusModal { - id: deleteAddressConfirm - property string address - property string name - // NOTE: the `text` property was created as a workaround because - // setting StatusBaseText.text to `qsTr("...").arg("...")` - // caused no text to render - property string text: qsTr("Are you sure you want to remove '%1' from your saved addresses?").arg(name) - anchors.centerIn: parent - header.title: qsTr("Are you sure?") - header.subTitle: name - contentItem: StatusBaseText { - anchors.centerIn: parent - height: contentHeight + topPadding + bottomPadding - text: deleteAddressConfirm.text - font.pixelSize: 15 - color: Theme.palette.directColor1 - wrapMode: Text.Wrap - topPadding: Style.current.padding - rightPadding: Style.current.padding - bottomPadding: Style.current.padding - leftPadding: Style.current.padding - } - rightButtons: [ - StatusButton { - text: qsTr("Cancel") - onClicked: deleteAddressConfirm.close() - }, - StatusButton { - type: StatusBaseButton.Type.Danger - objectName: "confirmDeleteSavedAddress" - text: qsTr("Delete") - onClicked: { - _internal.loading = true - _internal.error = RootStore.deleteSavedAddress(deleteAddressConfirm.address) - deleteAddressConfirm.close() - _internal.loading = false - } - } - ] - } - SavedAddressesError { id: errorMessage anchors.top: header.bottom @@ -242,6 +103,32 @@ Item { visible: listView.count > 0 spacing: 5 model: RootStore.savedAddresses - delegate: delegateSavedAddress + delegate: SavedAddressesDelegate { + name: model.name + address: model.address + store: RootStore + contactsStore: root.contactsStore + onOpenSendModal: root.sendModal.open(address); + saveAddress: function(name, address) { + _internal.saveAddress(name, address) + } + deleteSavedAddress: function(address) { + _internal.deleteSavedAddress(address) + } + } + } + + Component { + id: addEditSavedAddress + AddEditSavedAddressPopup { + id: addEditModal + anchors.centerIn: parent + onClosed: destroy() + contactsStore: root.contactsStore + onSave: { + _internal.saveAddress(name, address) + close() + } + } } } diff --git a/ui/imports/shared/controls/SavedAddressesDelegate.qml b/ui/imports/shared/controls/SavedAddressesDelegate.qml new file mode 100644 index 0000000000..ccd9575769 --- /dev/null +++ b/ui/imports/shared/controls/SavedAddressesDelegate.qml @@ -0,0 +1,176 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 + +import utils 1.0 + +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups 0.1 +import shared.controls 1.0 + +import "../popups" +import "../controls" + +StatusListItem { + id: root + + property var store + property var contactsStore + property string name + property string address + property var saveAddress: function (name, address) {} + property var deleteSavedAddress: function (address) {} + + signal openSendModal() + + implicitWidth: parent.width + + title: name + objectName: name + subTitle: name + " \u2022 " + Utils.getElidedCompressedPk(address) + color: "transparent" + border.color: Theme.palette.baseColor5 + //TODO uncomment when #6456 is fixed + //titleTextIcon: RootStore.favouriteAddress ? "star-icon" : "" + statusListItemComponentsSlot.spacing: 0 + property bool showButtons: sensor.containsMouse + + components: [ + StatusRoundButton { + icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1 + type: StatusRoundButton.Type.Tertiary + icon.name: "send" + onClicked: openSendModal() + }, + CopyToClipBoardButton { + id: copyButton + type: StatusRoundButton.Type.Tertiary + icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1 + store: root.store + textToCopy: root.address + }, + //TODO uncomment when #6456 is fixed + // StatusRoundButton { + // icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1 + // type: StatusRoundButton.Type.Tertiary + // icon.name: root.favouriteAddress ? "favourite" : "unfavourite" + // onClicked: { + // RootStore.setFavourite(); + // } + // }, + StatusRoundButton { + visible: !!root.name + icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1 + type: StatusRoundButton.Type.Tertiary + icon.name: "more" + onClicked: { + editDeleteMenu.openMenu(root.name, root.address); + } + }, + StatusRoundButton { + visible: !root.name + icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1 + type: StatusRoundButton.Type.Tertiary + icon.name: "add" + onClicked: { + Global.openPopup(addEditSavedAddress, + { + addAddress: true, + address: root.address + }) + } + } + ] + + StatusPopupMenu { + id: editDeleteMenu + property string contactName + property string contactAddress + function openMenu(name, address) { + contactName = name; + contactAddress = address; + popup(); + } + onClosed: { + contactName = ""; + contactAddress = ""; + } + StatusMenuItem { + text: qsTr("Edit") + objectName: "editroot" + assetSettings.name: "pencil-outline" + onTriggered: { + Global.openPopup(addEditSavedAddress, + { + edit: true, + address: editDeleteMenu.contactAddress, + name: editDeleteMenu.contactName + }) + } + } + StatusMenuSeparator { } + StatusMenuItem { + text: qsTr("Delete") + type: StatusMenuItem.Type.Danger + assetSettings.name: "delete" + objectName: "deleteSavedAddress" + onTriggered: { + deleteAddressConfirm.name = editDeleteMenu.contactName; + deleteAddressConfirm.address = editDeleteMenu.contactAddress; + deleteAddressConfirm.open() + } + } + } + + Component { + id: addEditSavedAddress + AddEditSavedAddressPopup { + id: addEditModal + anchors.centerIn: parent + onClosed: destroy() + contactsStore: root.contactsStore + onSave: { + root.saveAddress(name, address) + close() + } + } + } + + StatusModal { + id: deleteAddressConfirm + property string address + property string name + anchors.centerIn: parent + header.title: qsTr("Are you sure?") + header.subTitle: name + contentItem: StatusBaseText { + anchors.centerIn: parent + height: contentHeight + topPadding + bottomPadding + text: qsTr("Are you sure you want to remove '%1' from your saved addresses?").arg(name) + font.pixelSize: 15 + color: Theme.palette.directColor1 + wrapMode: Text.Wrap + topPadding: Style.current.padding + rightPadding: Style.current.padding + bottomPadding: Style.current.padding + leftPadding: Style.current.padding + } + rightButtons: [ + StatusButton { + text: qsTr("Cancel") + onClicked: deleteAddressConfirm.close() + }, + StatusButton { + type: StatusBaseButton.Type.Danger + objectName: "confirmDeleteSavedAddress" + text: qsTr("Delete") + onClicked: { + root.deleteSavedAddress(deleteAddressConfirm.address) + deleteAddressConfirm.close() + } + } + ] + } +} diff --git a/ui/imports/shared/controls/TransactionDelegate.qml b/ui/imports/shared/controls/TransactionDelegate.qml index d1a4ab8bd0..84ce6289da 100644 --- a/ui/imports/shared/controls/TransactionDelegate.qml +++ b/ui/imports/shared/controls/TransactionDelegate.qml @@ -25,12 +25,13 @@ StatusListItem { property string resolvedSymbol: root.symbol != "" ? root.symbol : "ETH" property string savedAddressName + state: "normal" asset.isImage: true asset.name: Style.png("tokens/%1".arg(resolvedSymbol)) - statusListItemTitle.font.weight: Font.Medium - title: isIncoming ? qsTr("Receive %1").arg(resolvedSymbol) : !!savedAddressName ? + title: modelData !== undefined && !!modelData ? + isIncoming ? qsTr("Receive %1").arg(resolvedSymbol) : !!savedAddressName ? qsTr("Send %1 to %2").arg(resolvedSymbol).arg(savedAddressName) : - qsTr("Send %1 to %2").arg(resolvedSymbol).arg(Utils.compactAddress(modelData.to, 4)) + qsTr("Send %1 to %2").arg(resolvedSymbol).arg(Utils.compactAddress(modelData.to, 4)): "" subTitle: shortTimeStamp inlineTagModel: 1 inlineTagDelegate: InformationTag { @@ -60,8 +61,8 @@ StatusListItem { height: 18 } StatusBaseText { + id: cryptoValueText text: "%1 %2".arg(cryptoValue).arg(resolvedSymbol) - font.pixelSize: 15 color: Theme.palette.directColor1 } } @@ -92,4 +93,41 @@ StatusListItem { height: 10 } } + + states: [ + State { + name: "normal" + PropertyChanges { + target: asset + width: 40 + height: 40 + } + PropertyChanges { + target: statusListItemTitle + font.weight: Font.Medium + font.pixelSize: 15 + } + PropertyChanges { + target: cryptoValueText + font.pixelSize: 15 + } + }, + State { + name: "big" + PropertyChanges { + target: asset + width: 50 + height: 50 + } + PropertyChanges { + target: statusListItemTitle + font.weight: Font.Bold + font.pixelSize: 17 + } + PropertyChanges { + target: cryptoValueText + font.pixelSize: 17 + } + } + ] } diff --git a/ui/imports/shared/controls/qmldir b/ui/imports/shared/controls/qmldir index e89bd65eb8..25d352f947 100644 --- a/ui/imports/shared/controls/qmldir +++ b/ui/imports/shared/controls/qmldir @@ -27,3 +27,5 @@ InformationTile 1.0 InformationTile.qml SocialLinkPreview 1.0 SocialLinkPreview.qml AssetsDetailsHeader 1.0 AssetsDetailsHeader.qml InformationTag 1.0 InformationTag.qml +TransactionDetailsHeader.qml 1.0 TransactionDetailsHeader.qml +SavedAddressesDelegate 1.0 SavedAddressesDelegate.qml diff --git a/ui/imports/shared/popups/AddEditSavedAddressPopup.qml b/ui/imports/shared/popups/AddEditSavedAddressPopup.qml new file mode 100644 index 0000000000..c575d4b97d --- /dev/null +++ b/ui/imports/shared/popups/AddEditSavedAddressPopup.qml @@ -0,0 +1,115 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQml.Models 2.14 + +import utils 1.0 +import shared.controls 1.0 +import shared.panels 1.0 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 +import StatusQ.Popups.Dialog 0.1 + +import "../stores" + +StatusDialog { + id: root + + property bool edit: false + property bool addAddress: false + property string address + property alias name: nameInput.text + property var contactsStore + + signal save(string name, string address) + + QtObject { + id: d + property int validationMode: root.edit ? + StatusInput.ValidationMode.Always + : StatusInput.ValidationMode.OnlyWhenDirty + property bool valid: addressInput.isValid && nameInput.valid // TODO: Add network preference and emoji + property bool dirty: nameInput.input.dirty + } + + width: 574 + height: 490 + + header: StatusDialogHeader { + headline.title: edit ? qsTr("Edit saved address") : qsTr("Add saved address") + headline.subtitle: edit ? name : "" + } + + onOpened: { + if(edit || addAddress) { + addressInput.input.text = root.address + } + nameInput.input.edit.forceActiveFocus(Qt.MouseFocusReason) + } + + Column { + width: parent.width + height: childrenRect.height + topPadding: Style.current.xlPadding + + spacing: Style.current.bigPadding + + StatusInput { + id: nameInput + implicitWidth: parent.width + input.edit.objectName: "savedAddressNameInput" + minimumHeight: 56 + maximumHeight: 56 + placeholderText: qsTr("Enter a name") + label: qsTr("Name") + validators: [ + StatusMinLengthValidator { + minLength: 1 + errorMessage: qsTr("Name must not be blank") + }, + StatusRegularExpressionValidator { + regularExpression: /^[^<>]+$/ + errorMessage: qsTr("This is not a valid account name") + } + ] + charLimit: 40 + validationMode: d.validationMode + } + + // To-Do use StatusInput within the below component + RecipientSelector { + id: addressInput + implicitWidth: parent.width + inputWidth: implicitWidth + accounts: RootStore.accounts + contactsStore: root.contactsStore + label: qsTr("Address") + input.textField.objectName: "savedAddressAddressInput" + input.placeholderText: qsTr("Enter ENS Name or Ethereum Address") + labelFont.pixelSize: 15 + labelFont.weight: Font.Normal + input.implicitHeight: 56 + input.textField.anchors.rightMargin: 0 + isSelectorVisible: false + addContactEnabled: false + onSelectedRecipientChanged: { + root.address = selectedRecipient.address + } + readOnly: root.edit || root.addAddress + wrongInputValidationError: qsTr("Please enter a valid ENS name OR Ethereum Address") + } + } + + footer: StatusDialogFooter { + rightButtons: ObjectModel { + StatusButton { + text: root.edit ? qsTr("Save") : qsTr("Add address") + enabled: d.valid && d.dirty + onClicked: root.save(name, address) + objectName: "addSavedAddress" + } + } + } +} diff --git a/ui/imports/shared/popups/qmldir b/ui/imports/shared/popups/qmldir index 42b0b00814..470ea5bd27 100644 --- a/ui/imports/shared/popups/qmldir +++ b/ui/imports/shared/popups/qmldir @@ -21,3 +21,4 @@ ProfilePopup 1.0 ProfilePopup.qml ImageCropWorkflow 1.0 ImageCropWorkflow.qml ImportCommunityPopup 1.0 ImportCommunityPopup.qml DisplayNamePopup 1.0 DisplayNamePopup.qml +AddEditSavedAddressPopup 1.0 AddEditSavedAddressPopup.qml diff --git a/ui/imports/shared/stores/RootStore.qml b/ui/imports/shared/stores/RootStore.qml index 9cc58afba1..c967e5919b 100644 --- a/ui/imports/shared/stores/RootStore.qml +++ b/ui/imports/shared/stores/RootStore.qml @@ -38,6 +38,7 @@ QtObject { property var historyTransactions: walletSectionTransactions.model property bool isNonArchivalNode: history.isNonArchivalNode + property var currentAccount: walletSectionCurrent property var walletTokensModule: walletSectionAllTokens property var tokens: walletSectionAllTokens.all property var accounts: walletSectionAccounts.model @@ -170,6 +171,10 @@ QtObject { return globalUtils.hex2Eth(value) } + function hex2Gwei(value) { + return globalUtils.hex2Gwei(value) + } + function findTokenSymbolByAddress(address) { return walletSectionAllTokens.findTokenSymbolByAddress(address) @@ -178,4 +183,20 @@ QtObject { function getNameForSavedWalletAddress(address) { return walletSectionSavedAddresses.getNameByAddress(address) } + + function createOrUpdateSavedAddress(name, address) { + return walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address) + } + + function deleteSavedAddress(address) { + return walletSectionSavedAddresses.deleteSavedAddress(address) + } + + function getLatestBlockNumber() { + return walletSectionTransactions.getLastTxBlockNumber() + } + + function getGasEthValue(gweiValue, gasLimit) { + return profileSectionModule.ensUsernamesModule.getGasEthValue(gweiValue, gasLimit) + } } diff --git a/ui/imports/shared/views/HistoryView.qml b/ui/imports/shared/views/HistoryView.qml index e4721a1d95..d618c6fb76 100644 --- a/ui/imports/shared/views/HistoryView.qml +++ b/ui/imports/shared/views/HistoryView.qml @@ -21,6 +21,8 @@ ColumnLayout { property int pageSize: 20 // number of transactions per page property bool isLoading: false + signal launchTransactionDetail(var transaction) + function fetchHistory() { if (RootStore.isFetchingHistory(historyView.account.address)) { isLoading = true @@ -78,7 +80,7 @@ ColumnLayout { onLoaded: { item.modelData = model } - } + } ScrollBar.vertical: StatusScrollBar {} @@ -99,7 +101,7 @@ ColumnLayout { StatusListItem { property var modelData height: 40 - title: Utils.formatShortDate(modelData.timestamp * 1000, RootStore.accountSensitiveSettings.is24hTimeFormat) + title: modelData !== undefined && !!modelData ? Utils.formatShortDate(modelData.timestamp * 1000, RootStore.accountSensitiveSettings.is24hTimeFormat) : "" statusListItemTitle.color: Theme.palette.baseColor1 color: Theme.palette.statusListItem.backgroundColor sensor.enabled: false @@ -109,21 +111,18 @@ ColumnLayout { Component { id: transactionDelegate TransactionDelegate { - isIncoming: modelData !== undefined ? modelData.to === account.address: false + isIncoming: modelData !== undefined && !!modelData ? modelData.to === account.address: false currentCurrency: RootStore.currentCurrency - cryptoValue: modelData !== undefined ? RootStore.hex2Eth(modelData.value) : "" + cryptoValue: modelData !== undefined && !!modelData ? RootStore.hex2Eth(modelData.value) : "" fiatValue: RootStore.getFiatValue(cryptoValue, resolvedSymbol, RootStore.currentCurrency) - networkIcon: modelData !== undefined ? RootStore.getNetworkIcon(modelData.chainId) : "" - networkColor: modelData !== undefined ? RootStore.getNetworkColor(modelData.chainId) : "" - networkName: modelData !== undefined ? RootStore.getNetworkShortName(modelData.chainId) : "" - symbol: modelData !== undefined ? RootStore.findTokenSymbolByAddress(modelData.contract) : "" - transferStatus: modelData !== undefined ? RootStore.hex2Dec(modelData.txStatus) : "" - shortTimeStamp: modelData !== undefined ? Utils.formatShortTime(modelData.timestamp * 1000, RootStore.accountSensitiveSettings.is24hTimeFormat) : "" - savedAddressName: modelData !== undefined ? RootStore.getNameForSavedWalletAddress(modelData.to) : "" - onClicked: { - transactionModal.transaction = modelData - transactionModal.open() - } + networkIcon: modelData !== undefined && !!modelData ? RootStore.getNetworkIcon(modelData.chainId) : "" + networkColor: modelData !== undefined && !!modelData ? RootStore.getNetworkColor(modelData.chainId) : "" + networkName: modelData !== undefined && !!modelData ? RootStore.getNetworkShortName(modelData.chainId) : "" + symbol: modelData !== undefined && !!modelData ? RootStore.findTokenSymbolByAddress(modelData.contract) : "" + transferStatus: modelData !== undefined && !!modelData ? RootStore.hex2Dec(modelData.txStatus) : "" + shortTimeStamp: modelData !== undefined && !!modelData ? Utils.formatShortTime(modelData.timestamp * 1000, RootStore.accountSensitiveSettings.is24hTimeFormat) : "" + savedAddressName: modelData !== undefined && !!modelData ? RootStore.getNameForSavedWalletAddress(modelData.to) : "" + onClicked: launchTransactionDetail(modelData) } } @@ -131,8 +130,4 @@ ColumnLayout { id: loadingImageComponent StatusLoadingIndicator {} } - - TransactionModal { - id: transactionModal - } } diff --git a/ui/imports/shared/views/TransactionDetailView.qml b/ui/imports/shared/views/TransactionDetailView.qml new file mode 100644 index 0000000000..b35cf79df6 --- /dev/null +++ b/ui/imports/shared/views/TransactionDetailView.qml @@ -0,0 +1,250 @@ +import QtQuick 2.13 +import QtQuick.Layouts 1.13 +import QtQuick.Controls 2.14 +import QtQuick.Window 2.12 + +import StatusQ.Components 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Popups 0.1 + +import shared.controls 1.0 +import utils 1.0 + +import "../stores" +import "../controls" + +Item { + id: root + + property var currentAccount: RootStore.currentAccount + property var contactsStore + property var transaction + property var sendModal + + signal goBack() + + QtObject { + id: d + readonly property bool isIncoming: root.transaction !== undefined && !!root.transaction ? root.transaction.to === currentAccount.address : false + readonly property string savedAddressNameTo: root.transaction !== undefined && !!root.transaction ? d.getNameForSavedWalletAddress(transaction.to) : "" + readonly property string savedAddressNameFrom: root.transaction !== undefined && !!root.transaction ? d.getNameForSavedWalletAddress(transaction.from): "" + readonly property string from: root.transaction !== undefined && !!root.transaction ? !!savedAddressNameFrom ? savedAddressNameFrom : Utils.compactAddress(transaction.from, 4): "" + readonly property string to: root.transaction !== undefined && !!root.transaction ? !!savedAddressNameTo ? savedAddressNameTo : Utils.compactAddress(transaction.to, 4): "" + + function getNameForSavedWalletAddress(address) { + return RootStore.getNameForSavedWalletAddress(address) + } + } + + StatusFlatButton { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + Layout.alignment: Qt.AlignTop + anchors.topMargin: -Style.current.xlPadding + anchors.leftMargin: -Style.current.xlPadding + icon.name: "arrow-left" + icon.width: 20 + icon.height: 20 + text: qsTr("Activity") + size: StatusBaseButton.Size.Large + onClicked: root.goBack() + } + + StatusScrollView { + anchors.top: backButton.bottom + anchors.left: parent.left + + width: parent.width + height: parent.height + contentHeight: column.height + contentWidth: parent.width + + Column { + id: column + width: parent.width - Style.current.xlPadding + + spacing: Style.current.bigPadding + + TransactionDelegate { + width: parent.width + + modelData: transaction + isIncoming: d.isIncoming + currentCurrency: RootStore.currentCurrency + cryptoValue: root.transaction !== undefined && !!root.transaction ? RootStore.hex2Eth(transaction.value): "" + fiatValue: root.transaction !== undefined && !!root.transaction ? RootStore.getFiatValue(cryptoValue, resolvedSymbol, RootStore.currentCurrency): "" + networkIcon: root.transaction !== undefined && !!root.transaction ? RootStore.getNetworkIcon(transaction.chainId): "" + networkColor: root.transaction !== undefined && !!root.transaction ? RootStore.getNetworkColor(transaction.chainId): "" + networkName: root.transaction !== undefined && !!root.transaction ? RootStore.getNetworkShortName(transaction.chainId): "" + symbol: root.transaction !== undefined && !!root.transaction ? RootStore.findTokenSymbolByAddress(transaction.contract): "" + transferStatus: root.transaction !== undefined && !!root.transaction ? RootStore.hex2Dec(transaction.txStatus): "" + shortTimeStamp: root.transaction !== undefined && !!root.transaction ? Utils.formatShortTime(transaction.timestamp * 1000, RootStore.accountSensitiveSettings.is24hTimeFormat): "" + savedAddressName: root.transaction !== undefined && !!root.transaction ? RootStore.getNameForSavedWalletAddress(transaction.to): "" + title: d.isIncoming ? qsTr("Received %1 %2 from %3").arg(cryptoValue).arg(resolvedSymbol).arg(d.from) : + qsTr("Sent %1 %2 to %3").arg(cryptoValue).arg(resolvedSymbol).arg(d.to) + sensor.enabled: false + color: Theme.palette.statusListItem.backgroundColor + state: "big" + } + + SavedAddressesDelegate { + width: parent.width + + name: d.isIncoming ? d.savedAddressNameFrom : d.savedAddressNameTo + address: root.transaction !== undefined && !!root.transaction ? d.isIncoming ? transaction.from : transaction.to : "" + title: d.isIncoming ? d.from : d.to + subTitle: root.transaction !== undefined && !!root.transaction ? d.isIncoming ? !!d.savedAddressNameFrom ? Utils.compactAddress(transaction.from, 4) : "" : !!d.savedAddressNameTo ? Utils.compactAddress(transaction.to, 4) : "": "" + store: RootStore + contactsStore: root.contactsStore + onOpenSendModal: root.sendModal.open(address); + saveAddress: function(name, address) { + RootStore.createOrUpdateSavedAddress(name, address) + } + deleteSavedAddress: function(address) { + RootStore.deleteSavedAddress(address) + } + } + + StatusExpandableItem { + width: parent.width + anchors.horizontalCenter: parent.horizontalCenter + + type: StatusExpandableItem.Type.Tertiary + expandable: true + primaryText: qsTr("Transaction summary") + expandableComponent: transactionSummary + separatorVisible: false + expanded: true + } + + StatusExpandableItem { + width: parent.width + anchors.horizontalCenter: parent.horizontalCenter + + type: StatusExpandableItem.Type.Tertiary + expandable: true + primaryText: qsTr("Fees") + expandableComponent: fees + expanded: true + } + + StatusListItem { + id: data + width: parent.width + anchors.horizontalCenter: parent.horizontalCenter + + color: "transparent" + border.width: 1 + border.color: Theme.palette.directColor8 + + statusListItemTitle.color: Theme.palette.baseColor1 + + title: qsTr("Data" ) + subTitle: root.transaction !== undefined && !!root.transaction ? root.transaction.input : "" + components: [ + CopyToClipBoardButton { + icon.width: 15 + icon.height: 15 + type: StatusRoundButton.Type.Tertiary + color: "transparent" + icon.color: data.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1 + store: RootStore + textToCopy: data.subTitle + } + ] + } + } + } + + + Component { + id: transactionSummary + Column { + id: column + width: parent.width + spacing: 8 + TransactionDelegate { + width: parent.width + modelData: transaction + isIncoming: d.isIncoming + currentCurrency: RootStore.currentCurrency + cryptoValue: root.transaction !== undefined && !!root.transaction ? RootStore.hex2Eth(transaction.value): "" + fiatValue: RootStore.getFiatValue(cryptoValue, resolvedSymbol, RootStore.currentCurrency) + networkIcon: root.transaction !== undefined && !!root.transaction ? RootStore.getNetworkIcon(transaction.chainId) : "" + networkColor: root.transaction !== undefined && !!root.transaction ? RootStore.getNetworkColor(transaction.chainId): "" + networkName: root.transaction !== undefined && !!root.transaction ? RootStore.getNetworkShortName(transaction.chainId): "" + symbol: root.transaction !== undefined && !!root.transaction ? RootStore.findTokenSymbolByAddress(transaction.contract): "" + transferStatus: root.transaction !== undefined && !!root.transaction ? RootStore.hex2Dec(transaction.txStatus): "" + shortTimeStamp: root.transaction !== undefined && !!root.transaction ? Utils.formatShortTime(transaction.timestamp * 1000, RootStore.accountSensitiveSettings.is24hTimeFormat): "" + savedAddressName: root.transaction !== undefined && !!root.transaction ? RootStore.getNameForSavedWalletAddress(transaction.to): "" + title: d.isIncoming ? qsTr("Received %1 %2 from %3").arg(cryptoValue).arg(resolvedSymbol).arg(d.from) : + qsTr("Sent %1 %2 to %3").arg(cryptoValue).arg(resolvedSymbol).arg(d.to) + sensor.enabled: false + color: Theme.palette.statusListItem.backgroundColor + border.width: 1 + border.color: Theme.palette.directColor8 + } + Row { + spacing: 8 + InformationTile { + maxWidth: parent.width + primaryText: qsTr("Time") + secondaryText: root.transaction !== undefined && !!root.transaction ? qsTr("%1 on %2"). + arg(Utils.formatShortTime(transaction.timestamp * 1000, RootStore.accountSensitiveSettings.is24hTimeFormat)). + arg(Utils.formatShortDate(transaction.timestamp * 1000, RootStore.accountSensitiveSettings.is24hTimeFormat)): "" + } + InformationTile { + maxWidth: parent.width + primaryText: qsTr("Confirmations") + secondaryText: { + if(root.transaction !== undefined && !!root.transaction ) + return Math.abs(RootStore.getLatestBlockNumber() - RootStore.hex2Dec(root.transaction.blockNumber)) + else + return "" + } + } + InformationTile { + maxWidth: parent.width + primaryText: qsTr("Nonce") + secondaryText: root.transaction !== undefined && !!root.transaction ? RootStore.hex2Dec(root.transaction.nonce) : "" + } + } + } + } + + Component { + id: fees + Column { + width: parent.width + spacing: 8 + Row { + spacing: 8 + InformationTile { + id: baseFee + maxWidth: parent.width + primaryText: qsTr("Base fee") + secondaryText: root.transaction !== undefined && !!root.transaction ? qsTr("%1 Gwei").arg(RootStore.hex2Gwei(root.transaction.baseGasFees)) : "" + } + InformationTile { + maxWidth: parent.width + primaryText: qsTr("Tip") + secondaryText: root.transaction !== undefined && !!root.transaction ? qsTr("%1 Gwei • Max: %2 Gwei"). + arg(RootStore.hex2Gwei(root.transaction.maxPriorityFeePerGas)). + arg(RootStore.hex2Gwei(root.transaction.maxFeePerGas)) : "" + secondaryLabel.textFormat: Text.RichText + } + } + InformationTile { + maxWidth: parent.width + primaryText: qsTr("Total fee") + secondaryText: root.transaction !== undefined && !!root.transaction ? qsTr("%1 Gwei • Max: %2 Gwei"). + arg(Utils.stripTrailingZeros(RootStore.hex2Gwei(root.transaction.totalFees))). + arg(Utils.stripTrailingZeros(RootStore.hex2Gwei(root.transaction.maxTotalFees))) : "" + secondaryLabel.textFormat: Text.RichText + } + } + } +} diff --git a/ui/imports/shared/views/qmldir b/ui/imports/shared/views/qmldir index 7b478465c6..b6cc69e0ac 100644 --- a/ui/imports/shared/views/qmldir +++ b/ui/imports/shared/views/qmldir @@ -11,3 +11,4 @@ ProfileView 1.0 ProfileView.qml AssetsView 1.0 AssetsView.qml HistoryView 1.0 HistoryView.qml AssetsDetailView 1.0 AssetsDetailView.qml +TransactionDetailView 1.0 TransactionDetailView.qml diff --git a/vendor/status-go b/vendor/status-go index 65be6f2b96..1485b3b4c8 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit 65be6f2b96d72161bfa23a384e9d0f2ccfc85c6a +Subproject commit 1485b3b4c808dd875d30f17e4be842fcc44c8d35