feat(@desktop/wallet): Implement Transaction details as per new design

fixes #7214
This commit is contained in:
Khushboo Mehta 2022-09-05 11:15:47 +02:00 committed by Khushboo-dev-cpp
parent cdfbb4ac87
commit 928e1999d9
23 changed files with 759 additions and 181 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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())

View File

@ -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()

View File

@ -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()

View File

@ -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.

View File

@ -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 ""

@ -1 +1 @@
Subproject commit 7108ac87cffe529d011ec1c14473630e52980282
Subproject commit c3bbb396ecd5ad25df565d64429aaba869df4b49

View File

@ -54,6 +54,7 @@ Item {
id: walletContainer
RightTabView {
store: root.store
contactsStore: root.contactsStore
sendModal: root.sendModal
}
}

View File

@ -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 {

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}
]
}
}

View File

@ -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
}
}
]
}

View File

@ -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

View File

@ -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"
}
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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 <font color=\"#939BA1\">on</font> %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 <font color=\"#939BA1\">&#8226; Max: %2 Gwei</font>").
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 <font color=\"#939BA1\">&#8226; Max: %2 Gwei</font>").
arg(Utils.stripTrailingZeros(RootStore.hex2Gwei(root.transaction.totalFees))).
arg(Utils.stripTrailingZeros(RootStore.hex2Gwei(root.transaction.maxTotalFees))) : ""
secondaryLabel.textFormat: Text.RichText
}
}
}
}

View File

@ -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

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 65be6f2b96d72161bfa23a384e9d0f2ccfc85c6a
Subproject commit 1485b3b4c808dd875d30f17e4be842fcc44c8d35