import QtQuick 2.13 import QtQuick.Layouts 1.13 import QtQuick.Controls 2.14 import QtQuick.Window 2.12 import QtGraphicalEffects 1.15 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 shared.panels 1.0 import utils 1.0 import shared.stores 1.0 import "../controls" import "../popups" import "../stores" as WalletStores import ".." import "../panels" Item { id: root property var overview: WalletStores.RootStore.overview property var contactsStore property var transaction property var sendModal property bool showAllAccounts: false readonly property bool isTransactionValid: transaction !== undefined && !!transaction onTransactionChanged: { d.decodedInputData = "" if (!transaction || !transaction.input || !RootStore.history) return d.loadingInputDate = true RootStore.history.fetchDecodedTxData(transaction.txHash, transaction.input) } QtObject { id: d readonly property bool isIncoming: transactionType === Constants.TransactionType.Received readonly property string networkShortName: root.isTransactionValid ? RootStore.getNetworkShortName(transaction.chainId) : "" readonly property string networkIcon: isTransactionValid ? RootStore.getNetworkIcon(transaction.chainId) : "" readonly property int blockNumber: root.isTransactionValid ? RootStore.hex2Dec(root.transaction.blockNumber) : 0 readonly property int toBlockNumber: 0 // TODO fill when bridge data is implemented readonly property string toNetworkIcon: "" // TODO fill when bridge data is implemented readonly property string toNetworkShortName: "" // TODO fill when bridge data is implemented readonly property string symbol: isTransactionValid ? transaction.symbol : "" readonly property string inSymbol: isTransactionValid ? transaction.inSymbol : "" readonly property string outSymbol: isTransactionValid ? transaction.outSymbol : "" readonly property var multichainNetworks: [] // TODO fill icon for networks for multichain readonly property string fiatValueFormatted: { if (!root.isTransactionValid || transactionHeader.isMultiTransaction || !symbol) return "" return RootStore.formatCurrencyAmount(transactionHeader.fiatValue, RootStore.currentCurrency) } readonly property string cryptoValueFormatted: { if (!root.isTransactionValid || transactionHeader.isMultiTransaction) return "" const formatted = RootStore.formatCurrencyAmount(transaction.amount, transaction.symbol) return symbol || !transaction.contract ? formatted : "%1 (%2)".arg(formatted).arg(Utils.compactAddress(transaction.contract, 4)) } readonly property string outFiatValueFormatted: { if (!root.isTransactionValid || !transactionHeader.isMultiTransaction || !outSymbol) return "" return RootStore.formatCurrencyAmount(transactionHeader.outFiatValue, RootStore.currentCurrency) } readonly property string outCryptoValueFormatted: { if (!root.isTransactionValid || !transactionHeader.isMultiTransaction) return "" const formatted = RootStore.formatCurrencyAmount(transaction.outAmount, transaction.outSymbol) return outSymbol || !transaction.contract ? formatted : "%1 (%2)".arg(formatted).arg(Utils.compactAddress(transaction.contract, 4)) } readonly property real feeEthValue: root.isTransactionValid ? RootStore.getGasEthValue(transaction.totalFees.amount, 1) : 0 // TODO use directly? readonly property real feeFiatValue: root.isTransactionValid ? RootStore.getFiatValue(d.feeEthValue, Constants.ethToken, RootStore.currentCurrency) : 0 // TODO use directly? readonly property int transactionType: root.isTransactionValid ? transaction.txType : Constants.TransactionType.Send readonly property string toNetworkName: "" // TODO fill network name for bridge property string decodedInputData: "" property bool loadingInputDate: false function getNameForSavedWalletAddress(address) { return RootStore.getNameForSavedWalletAddress(address) } function retryTransaction() { // TODO handle failed transaction retry } } Connections { target: RootStore.history function onTxDecoded(txHash: string, dataDecoded: string) { if (!root.isTransactionValid || txHash !== root.transaction.txHash) return if (!dataDecoded) { d.loadingInputDate = false return } try { const decodedObject = JSON.parse(dataDecoded) let text = qsTr("Function: %1").arg(decodedObject.signature) text += "\n" + qsTr("MethodID: %1").arg(decodedObject.id) for (const [key, value] of Object.entries(decodedObject.inputs)) { text += "\n[%1]: %2".arg(key).arg(value) } d.decodedInputData = text } catch(e) { console.error("Failed to parse decoded tx data. Data:", dataDecoded) } d.loadingInputDate = false } } StatusScrollView { id: scrollView anchors.fill: parent contentWidth: availableWidth Column { id: column width: scrollView.availableWidth spacing: Style.current.xlPadding + Style.current.halfPadding Column { width: parent.width spacing: Style.current.bigPadding TransactionDelegate { id: transactionHeader objectName: "transactionDetailHeader" width: parent.width leftPadding: 0 sensor.enabled: false color: Theme.palette.transparent state: "header" showAllAccounts: root.showAllAccounts modelData: transaction timeStampText: root.isTransactionValid ? qsTr("Signed at %1").arg(LocaleUtils.formatDateTime(transaction.timestamp * 1000, Locale.LongFormat)): "" rootStore: RootStore walletRootStore: WalletStores.RootStore onRetryClicked: d.retryTransaction() } Separator { } } WalletTxProgressBlock { id: progressBlock width: Math.min(513, root.width) error: transactionHeader.transactionStatus === Constants.TransactionStatus.Failed isLayer1: root.isTransactionValid && RootStore.getNetworkLayer(root.transaction.chainId) == 1 confirmations: root.isTransactionValid ? Math.abs(WalletStores.RootStore.getLatestBlockNumber(root.transaction.chainId) - d.blockNumber): 0 chainName: transactionHeader.networkName timeStamp: root.isTransactionValid ? transaction.timestamp: "" } Separator { width: progressBlock.width } WalletNftPreview { visible: root.isTransactionValid && transactionHeader.isNFT && !!transaction.nftImageUrl width: Math.min(304, progressBlock.width) nftName: root.isTransactionValid ? transaction.nftName : "" nftUrl: root.isTransactionValid && !!transaction.nftImageUrl ? transaction.nftImageUrl : "" strikethrough: d.transactionType === Constants.TransactionType.Destroy tokenId: root.isTransactionValid ? transaction.tokenID : "" contractAddress: root.isTransactionValid ? transaction.contract : "" } Column { width: progressBlock.width spacing: 0 StatusBaseText { width: parent.width font.pixelSize: 15 color: Theme.palette.directColor5 text: qsTr("Transaction summary") elide: Text.ElideRight } Item { width: parent.width height: Style.current.smallPadding } DetailsPanel { RowLayout { spacing: 0 width: parent.width height: fromNetworkTile.visible || toNetworkTile.visible ? 85 : 0 TransactionDataTile { id: fromNetworkTile Layout.fillWidth: true Layout.fillHeight: true title: qsTr("From") subTitle: { switch(d.transactionType) { case Constants.TransactionType.Swap: return d.outSymbol case Constants.TransactionType.Bridge: return transactionHeader.networkName default: return "" } } asset.name: { switch(d.transactionType) { case Constants.TransactionType.Swap: return !!d.outSymbol ? Constants.tokenIcon(d.outSymbol) : "" case Constants.TransactionType.Bridge: return !!d.networkIcon ? Style.svg(d.networkIcon) : "" default: return "" } } visible: !!subTitle } TransactionDataTile { id: toNetworkTile Layout.fillWidth: true Layout.fillHeight: true title: qsTr("To") subTitle: { switch(d.transactionType) { case Constants.TransactionType.Swap: return d.inSymbol case Constants.TransactionType.Bridge: return d.toNetworkName default: return "" } } asset.name: { switch(d.transactionType) { case Constants.TransactionType.Swap: return !!d.inSymbol ? Constants.tokenIcon(d.inSymbol) : "" case Constants.TransactionType.Bridge: return !!d.toNetworkIcon ? Style.svg(d.toNetworkIcon) : "" default: return "" } } visible: !!subTitle } } TransactionAddressTile { width: parent.width title: d.transactionType === Constants.TransactionType.Swap || d.transactionType === Constants.TransactionType.Bridge ? qsTr("In") : qsTr("From") addresses: root.isTransactionValid ? [root.transaction.sender] : [] contactsStore: root.contactsStore rootStore: WalletStores.RootStore onButtonClicked: { if (d.transactionType === Constants.TransactionType.Swap || d.transactionType === Constants.TransactionType.Bridge) { addressMenu.openEthAddressMenu(this, addresses[0], d.networkShortName) } else { addressMenu.openSenderMenu(this, addresses[0], d.networkShortName) } } } TransactionDataTile { id: contractDeploymentTile readonly property bool hasValue: root.isTransactionValid && !!root.transaction.contract && transactionHeader.transactionStatus !== Constants.TransactionStatus.Pending && transactionHeader.transactionStatus !== Constants.TransactionStatus.Failed width: parent.width title: qsTr("To") visible: d.transactionType === Constants.TransactionType.ContractDeployment subTitle: { if (transactionHeader.transactionStatus === Constants.TransactionStatus.Failed) { return qsTr("Contract address not created") } else if (!hasValue) { return qsTr("Awaiting contract address...") } return qsTr("Contract created") + "\n" + transaction.contract } buttonIconName: hasValue ? "more" : "" statusListItemSubTitle.customColor: hasValue ? Theme.palette.directColor1 : Theme.palette.directColor5 onButtonClicked: addressMenu.openContractMenu(this, transaction.contract, transactionHeader.networkName, d.symbol) components: [ Loader { anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: Style.current.halfPadding active: transactionHeader.transactionStatus === Constants.TransactionStatus.Pending width: active ? implicitWidth : 0 sourceComponent: StatusLoadingIndicator { } } ] } TransactionAddressTile { width: parent.width title: qsTr("To") addresses: root.isTransactionValid && visible ? [root.transaction.recipient] : [] contactsStore: root.contactsStore rootStore: WalletStores.RootStore onButtonClicked: addressMenu.openReceiverMenu(this, addresses[0], d.networkShortName) visible: d.transactionType !== Constants.TransactionType.ContractDeployment && d.transactionType !== Constants.TransactionType.Swap && d.transactionType !== Constants.TransactionType.Bridge && d.transactionType !== Constants.TransactionType.Destroy } TransactionDataTile { width: parent.width title: qsTr("Using") buttonIconName: "external" subTitle: "" // TODO fill protocol name for Swap and Bridge asset.name: "" // TODO fill protocol icon for Bridge and Swap e.g. Style.svg("network/Network=Arbitrum") onButtonClicked: { // TODO handle } visible: !!subTitle } TransactionDataTile { width: parent.width title: qsTr("%1 Tx hash").arg(transactionHeader.networkName) subTitle: root.isTransactionValid ? root.transaction.txHash : "" visible: !!subTitle buttonIconName: "more" onButtonClicked: addressMenu.openTxMenu(this, subTitle, d.networkShortName) } TransactionDataTile { width: parent.width title: qsTr("%1 Tx hash").arg(d.toNetworkName) subTitle: "" // TODO fill tx hash for Bridge visible: !!subTitle buttonIconName: "more" onButtonClicked: addressMenu.openTxMenu(this, subTitle, d.toNetworkShortName) } TransactionContractTile { // Used for Bridge and Swap to display 'From' network Protocol contract address address: "" // TODO fill protocol contract address for 'from' network for Bridge and Swap symbol: "" // TODO fill protocol name for Bridge and Swap networkName: transactionHeader.networkName shortNetworkName: d.networkShortName visible: !!subTitle && (d.transactionType === Constants.TransactionType.Bridge || d.transactionType === Constants.TransactionType.Swap) } TransactionContractTile { // Used to display contract address for any network address: root.isTransactionValid ? transaction.contract : "" symbol: { if (!root.isTransactionValid) return "" return d.symbol ? d.symbol : "(%1)".arg(Utils.compactAddress(transaction.contract, 4)) } networkName: transactionHeader.networkName shortNetworkName: d.networkShortName visible: !!subTitle && d.transactionType !== Constants.TransactionType.ContractDeployment } TransactionContractTile { // Used for Bridge to display 'To' network Protocol contract address address: "" // TODO fill protocol contract address for 'to' network for Bridge symbol: "" // TODO fill protocol name for Bridge networkName: d.toNetworkName shortNetworkName: d.toNetworkShortName visible: !!subTitle && d.transactionType === Constants.TransactionType.Bridge } TransactionContractTile { // Used for Bridge and Swap to display 'To' network token contract address address: { if (!root.isTransactionValid) return "" switch(d.transactionType) { case Constants.TransactionType.Swap: return "" // TODO fill swap contract address for Swap case Constants.TransactionType.Bridge: return "" // TODO fill swap token's contract address for 'to' network for Bridge default: return "" } } symbol: { if (!root.isTransactionValid) return "" switch(d.transactionType) { case Constants.TransactionType.Swap: return d.inSymbol case Constants.TransactionType.Bridge: return d.outSymbol default: return "" } } networkName: d.toNetworkName shortNetworkName: d.toNetworkShortName visible: root.isTransactionValid && !!subTitle } } Item { width: parent.width height: Style.current.bigPadding } DetailsPanel { width: progressBlock.width RowLayout { width: parent.width height: networkNameTile.statusListItemSubTitle.lineCount > 1 ? 85 : 70 spacing: 0 TransactionDataTile { id: multichainNetworksTile Layout.fillHeight: true Layout.fillWidth: true title: qsTr("Networks") visible: d.multichainNetworks.length > 0 Row { anchors { top: parent.top topMargin: multichainNetworksTile.statusListItemTitleArea.height + multichainNetworksTile.topPadding left: parent.left leftMargin: multichainNetworksTile.leftPadding } spacing: -4 Repeater { model: d.multichainNetworks delegate: StatusRoundedImage { width: 20 height: 20 visible: image.source !== "" border.width: index === 0 ? 0 : 1 border.color: Theme.palette.white image.source: Style.svg("tiny/" + modelData) z: index + 1 } } } } TransactionDataTile { id: networkNameTile Layout.fillHeight: true Layout.fillWidth: true title: qsTr("Network") subTitle: transactionHeader.networkName asset.name: !!d.networkIcon ? Style.svg(d.networkIcon) : "" subTitleBadgeLoaderAlignment: Qt.AlignTop smallIcon: true visible: d.transactionType !== Constants.TransactionType.Bridge } TransactionDataTile { Layout.fillHeight: true Layout.fillWidth: true title: qsTr("Token format") subTitle: root.isTransactionValid ? transaction.type.toUpperCase() : "" visible: !!subTitle } TransactionDataTile { Layout.fillHeight: true Layout.fillWidth: true title: qsTr("Nonce") subTitle: root.isTransactionValid ? RootStore.hex2Dec(root.transaction.nonce) : "" visible: !!subTitle } } TransactionDataTile { width: parent.width height: d.loadingInputDate ? 112 : Math.min(implicitHeight + bottomPadding, 112) title: qsTr("Input data") // Input string can be really long. Getting substring just for 3+ lines to speedup formatting. subTitle: { if (d.loadingInputDate) { return "" } else if (!!d.decodedInputData) { return d.decodedInputData.substring(0, 200) } else if (root.isTransactionValid) { return String(root.transaction.input).substring(0, 200) } return "" } visible: !!subTitle || d.loadingInputDate buttonIconName: d.loadingInputDate ? "" : "more" statusListItemSubTitle.maximumLineCount: 4 statusListItemSubTitle.lineHeight: 1.21 statusListItemSubTitle.layer.enabled: statusListItemSubTitle.lineCount > 3 statusListItemSubTitle.layer.effect: OpacityMask { maskSource: Rectangle { width: 10 height: 10 gradient: Gradient { GradientStop { position: 0.0; color: "#f00" } GradientStop { position: 0.4; color: "#a0ff0000" } GradientStop { position: 0.75; color: "#00ff0000" } } } } tertiaryTitle: !d.loadingInputDate && !d.decodedInputData ? qsTr("Data could not be decoded") : "" statusListItemTertiaryTitle.anchors.baseline: statusListItemTitle.baseline statusListItemTertiaryTitle.font: statusListItemTitle.font onButtonClicked: addressMenu.openInputDataMenu(this, !!d.decodedInputData ? d.decodedInputData : root.transaction.input) Loader { anchors { left: parent.left bottom: parent.bottom right: parent.right margins: 12 } active: d.loadingInputDate sourceComponent: Column { spacing: 10 Repeater { model: 3 LoadingComponent { anchors { left: parent.left right: index === 2 ? parent.horizontalCenter : parent.right } height: 11 radius: 4 enabled: false } } } } } TransactionDataTile { width: parent.width title: !!transactionHeader.networkName ? qsTr("Included in Block on %1").arg(transactionHeader.networkName) : qsTr("Included on Block") subTitle: d.blockNumber tertiaryTitle: root.isTransactionValid ? LocaleUtils.formatDateTime(transaction.timestamp * 1000, Locale.LongFormat) : "" visible: d.blockNumber > 0 } TransactionDataTile { width: parent.width title: !!d.toNetworkName ? qsTr("Included in Block on %1").arg(d.toNetworkName) : qsTr("Included on Block") subTitle: d.toBlockNumber tertiaryTitle: root.isTransactionValid ? LocaleUtils.formatDateTime(transaction.timestamp * 1000, Locale.LongFormat) : "" visible: d.toBlockNumber > 0 } } } Column { width: progressBlock.width spacing: Style.current.smallPadding visible: !(transactionHeader.isNFT && d.isIncoming) RowLayout { width: parent.width StatusBaseText { Layout.alignment: Qt.AlignLeft font.pixelSize: 15 color: Theme.palette.directColor5 text: qsTr("Values") elide: Text.ElideRight } StatusBaseText { Layout.alignment: Qt.AlignRight font.pixelSize: 15 color: Theme.palette.directColor5 text: root.isTransactionValid ? qsTr("as of %1").arg(LocaleUtils.formatDateTime(transaction.timestamp * 1000, Locale.LongFormat)) : "" elide: Text.ElideRight } } DetailsPanel { TransactionDataTile { width: parent.width title: qsTr("Amount sent") subTitle: transactionHeader.isMultiTransaction ? d.outCryptoValueFormatted : d.cryptoValueFormatted tertiaryTitle: transactionHeader.isMultiTransaction ? d.outFiatValueFormatted : d.fiatValueFormatted visible: { if (transactionHeader.isNFT) return false switch(d.transactionType) { case Constants.TransactionType.Send: case Constants.TransactionType.Swap: case Constants.TransactionType.Bridge: return true default: return false } } } TransactionDataTile { width: parent.width title: transactionHeader.transactionStatus === Constants.TransactionStatus.Pending ? qsTr("Amount to receive") : qsTr("Amount received") subTitle: { if (!root.isTransactionValid || transactionHeader.isNFT) return "" const type = d.transactionType if (type === Constants.TransactionType.Swap) { return RootStore.formatCurrencyAmount(transactionHeader.inCryptoValue, d.inSymbol) } else if (type === Constants.TransactionType.Bridge) { // Reduce crypto value by fee value const valueInCrypto = RootStore.getCryptoValue(transactionHeader.fiatValue - d.feeFiatValue, d.inSymbol, RootStore.currentCurrency) return RootStore.formatCurrencyAmount(valueInCrypto, d.inSymbol) } return "" } tertiaryTitle: { const type = d.transactionType if (type === Constants.TransactionType.Swap) { return RootStore.formatCurrencyAmount(transactionHeader.inFiatValue, RootStore.currentCurrency) } else if (type === Constants.TransactionType.Bridge) { return RootStore.formatCurrencyAmount(transactionHeader.fiatValue - d.feeFiatValue, RootStore.currentCurrency) } return "" } visible: !!subTitle } TransactionDataTile { width: parent.width title: d.symbol ? qsTr("Fees") : qsTr("Estimated max fee") subTitle: { if (!root.isTransactionValid || transactionHeader.isNFT) return "" if (!d.symbol) { const maxFeeEth = RootStore.getGasEthValue(transaction.maxTotalFees.amount, 1) return RootStore.formatCurrencyAmount(maxFeeEth, Constants.ethToken) } switch(d.transactionType) { case Constants.TransactionType.Send: case Constants.TransactionType.Swap: case Constants.TransactionType.Bridge: return LocaleUtils.currencyAmountToLocaleString(root.transaction.totalFees) default: return "" } } tertiaryTitle: { if (!subTitle) return "" let fiatValue if (!d.symbol) { const maxFeeEth = RootStore.getGasEthValue(transaction.maxTotalFees.amount, 1) fiatValue = RootStore.getFiatValue(maxFeeEth, Constants.ethToken, RootStore.currentCurrency) } else { fiatValue = d.feeFiatValue } return RootStore.formatCurrencyAmount(fiatValue, RootStore.currentCurrency) } visible: !!subTitle } TransactionDataTile { width: parent.width readonly property bool fieldIsHidden: (transactionHeader.isNFT && d.isIncoming) || !d.symbol readonly property bool showMaxFee: d.transactionType === Constants.TransactionType.ContractDeployment && transactionHeader.transactionStatus === Constants.TransactionStatus.Pending readonly property bool showFee: d.transactionType === Constants.TransactionType.Destroy || transactionHeader.isNFT || d.transactionType === Constants.TransactionType.ContractDeployment readonly property bool showValue: d.transactionType === Constants.TransactionType.Receive || (d.transactionType === Constants.TransactionType.Buy && progressBlock.isLayer1) // NOTE Using fees in this tile because of same higlight and color settings as Total title: { if (showMaxFee) { return qsTr("Estimated max fee") } else if (showFee) { return qsTr("Fees") } return qsTr("Total") } subTitle: { if (fieldIsHidden) return "" if (showMaxFee) { const maxFeeEth = RootStore.getGasEthValue(transaction.maxTotalFees.amount, 1) return RootStore.formatCurrencyAmount(maxFeeEth, Constants.ethToken) } else if (showFee) { return RootStore.formatCurrencyAmount(d.feeEthValue, Constants.ethToken) } else if (showValue) { return d.cryptoValueFormatted } const cryptoValue = transactionHeader.isMultiTransaction ? d.outCryptoValueFormatted : d.cryptoValueFormatted return "%1 + %2".arg(cryptoValue).arg(RootStore.formatCurrencyAmount(d.feeEthValue, Constants.ethToken)) } tertiaryTitle: { if (fieldIsHidden) return "" if (showMaxFee) { const maxFeeEth = RootStore.getGasEthValue(transaction.maxTotalFees.amount, 1) const maxFeeFiat = RootStore.getFiatValue(d.feeEthValue, "ETH", RootStore.currentCurrency) return RootStore.formatCurrencyAmount(maxFeeFiat, RootStore.currentCurrency) } else if (showFee) { return RootStore.formatCurrencyAmount(d.feeFiatValue, RootStore.currentCurrency) } else if (showValue) { return d.fiatValueFormatted } const fiatValue = transactionHeader.isMultiTransaction ? transactionHeader.outFiatValue : transactionHeader.fiatValue return RootStore.formatCurrencyAmount(fiatValue + d.feeFiatValue, RootStore.currentCurrency) } visible: !!subTitle highlighted: true statusListItemTertiaryTitle.customColor: Theme.palette.directColor1 } } } Separator { width: progressBlock.width } RowLayout { width: progressBlock.width visible: root.isTransactionValid spacing: 8 StatusButton { Layout.fillWidth: true Layout.preferredHeight: copyDetailsButton.height text: qsTr("Repeat transaction") size: StatusButton.Small visible: root.isTransactionValid && !root.overview.isWatchOnlyAccount && d.transactionType === TransactionDelegate.Send onClicked: { root.sendModal.open(root.transaction.to) // TODO handle other types } } StatusButton { id: copyDetailsButton Layout.fillWidth: true text: qsTr("Copy details") icon.name: "copy" icon.width: 20 icon.height: 20 size: StatusButton.Small onClicked: RootStore.copyToClipboard(transactionHeader.getDetailsString()) } } } } TransactionAddressMenu { id: addressMenu areTestNetworksEnabled: WalletStores.RootStore.areTestNetworksEnabled contactsStore: root.contactsStore onOpenSendModal: (address) => root.sendModal.open(address) } component DetailsPanel: Item { width: parent.width height: { // Using childrenRect and transactionvalid properties to refresh this binding if (!isTransactionValid || detailsColumn.childrenRect.height === 0) return 0 // Height is calculated from visible children because Column doesn't handle // visibility change properly and childrenRect.height gives different values // comparing to manual check var visibleHeight = 0 for (var i = 0 ; i < detailsColumn.children.length ; i++) { if (detailsColumn.children[i].visible) visibleHeight += detailsColumn.children[i].height } return visibleHeight } default property alias content: detailsColumn.children Rectangle { id: tileBackground anchors.fill: parent color: Style.current.transparent radius: 8 border.width: 1 border.color: Style.current.separator } Column { id: detailsColumn anchors.fill: parent anchors.margins: 1 spacing: 0 layer.enabled: true layer.effect: OpacityMask { // Separate rectangle is used as mask because background rectangle must be transaprent maskSource: Rectangle { width: tileBackground.width height: tileBackground.height radius: tileBackground.radius } } } } component TransactionContractTile: TransactionDataTile { property string networkName: "" property string symbol: "" property string address: "" property string shortNetworkName: "" width: parent.width title: visible ? qsTr("%1 %2 contract address").arg(networkName).arg(symbol) : "" subTitle: !!address && !/0x0+$/.test(address) ? address : "" buttonIconName: "more" visible: !!subTitle onButtonClicked: addressMenu.openContractMenu(this, address, shortNetworkName, symbol) } }