import QtQuick 2.15 import QtQuick.Layouts 1.15 import QtQml 2.15 import StatusQ.Components 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core 0.1 import StatusQ.Controls 0.1 import utils 1.0 import shared 1.0 /*! \qmltype TransactionDelegate \inherits StatusListItem \inqmlmodule shared.controls \since shared.controls 1.0 \brief Delegate for transaction activity list Delegate to display transaction activity data. \qml TransactionDelegate { id: delegate width: ListView.view.width modelData: model swapCryptoValue: 0.18 swapFiatValue: 340 swapSymbol: "SNT" timeStampText: LocaleUtils.formatRelativeTimestamp(modelData.timestamp * 1000) cryptoValue: 0.1234 fiatValue: 123123 currentCurrency: "USD" networkName: "Optimism" symbol: "ETH" bridgeNetworkName: "Mainnet" feeFiatValue: 10.34 feeCryptoValue: 0.013 transactionStatus: TransactionDelegate.Pending transactionType: TransactionDelegate.Send rootStore: RootStore loading: isModelDataValid && modelData.loadingTransaction } \endqml Additional usages should be handled using states. */ StatusListItem { id: root signal retryClicked() property var modelData property string symbol property string swapSymbol // TODO fill when swap data is implemented property int transactionStatus property string currentCurrency property double cryptoValue property double swapCryptoValue // TODO fill when swap data is implemented property double fiatValue property double swapFiatValue // TODO fill when swap data is implemented property double feeCryptoValue // TODO fill when bridge data is implemented property double feeFiatValue // TODO fill when bridge data is implemented property string networkIcon property string networkColor property string networkName property string bridgeNetworkName // TODO fill when bridge data is implemented property string timeStampText property string addressNameTo property string addressNameFrom property var rootStore property var walletRootStore readonly property bool isModelDataValid: modelData !== undefined && !!modelData readonly property bool isNFT: isModelDataValid && modelData.isNFT readonly property string transactionValue: { if (!isModelDataValid) return qsTr("N/A") if (root.isNFT) { return modelData.nftName ? modelData.nftName : "#" + modelData.tokenID } else { return root.rootStore.formatCurrencyAmount(cryptoValue, symbol) } } readonly property string swapTransactionValue: { if (!isModelDataValid) { return qsTr("N/A") } return root.rootStore.formatCurrencyAmount(swapCryptoValue, swapSymbol) } readonly property string tokenImage: { if (!isModelDataValid) return "" if (root.isNFT) { return modelData.nftImageUrl ? modelData.nftImageUrl : "" } else { return Constants.tokenIcon(root.symbol) } } readonly property string swapTokenImage: { if (!isModelDataValid) return "" return Constants.tokenIcon(root.swapSymbol) } readonly property string toAddress: !!addressNameTo ? addressNameTo : isModelDataValid ? Utils.compactAddress(modelData.recipient, 4) : "" readonly property string fromAddress: !!addressNameFrom ? addressNameFrom : isModelDataValid ? Utils.compactAddress(modelData.sender, 4) : "" property StatusAssetSettings statusIconAsset: StatusAssetSettings { width: 12 height: 12 bgWidth: width + 2 bgHeight: bgWidth bgRadius: bgWidth / 2 bgColor: root.color color: "transparent" name: { switch(root.transactionStatus) { case Constants.TransactionStatus.Pending: return Style.svg("transaction/pending") case Constants.TransactionStatus.Complete: return Style.svg("transaction/verified") case Constants.TransactionStatus.Finished: return Style.svg("transaction/finished") case Constants.TransactionStatus.Failed: return Style.svg("transaction/failed") default: return "" } } } property StatusAssetSettings tokenIconAsset: StatusAssetSettings { width: 18 height: 18 bgWidth: width bgHeight: height bgColor: "transparent" color: "transparent" isImage: !loading name: root.tokenImage isLetterIdenticon: loading } QtObject { id: d property int loadingPixelSize: 13 property int datePixelSize: 12 property int titlePixelSize: 15 property int subtitlePixelSize: 13 } function getDetailsString() { let details = "" const endl = "\n" const endl2 = endl + endl const type = modelData.txType const feeEthValue = modelData.totalFees ? rootStore.getGasEthValue(modelData.totalFees.amount, 1) : 0 // TITLE switch (type) { case Constants.TransactionType.Send: details += qsTr("Send transaction details" + endl2) break case Constants.TransactionType.Receive: details += qsTr("Receive transaction details") + endl2 break case Constants.TransactionType.Buy: details += qsTr("Buy transaction details") + endl2 break case Constants.TransactionType.Sell: details += qsTr("Sell transaction details") + endl2 break case Constants.TransactionType.Destroy: details += qsTr("Destroy transaction details") + endl2 break case Constants.TransactionType.Swap: details += qsTr("Swap transaction details") + endl2 break case Constants.TransactionType.Bridge: details += qsTr("Bridge transaction details") + endl2 break default: break } details += qsTr("Summary") + endl switch(modelData.txType) { case Constants.TransactionType.Buy: case Constants.TransactionType.Sell: case Constants.TransactionType.Destroy: case Constants.TransactionType.Swap: case Constants.TransactionType.Bridge: details += subTitle + endl2 break default: details += qsTr("%1 from %2 to %3 via %4").arg(transactionValue).arg(fromAddress).arg(toAddress).arg(networkName) + endl2 break } if (root.isNFT) { details += qsTr("Token ID") + endl + modelData.tokenID + endl2 details += qsTr("Token name") + endl + modelData.nftName + endl2 } // PROGRESS const isLayer1 = rootStore.getNetworkLayer(modelData.chainId) === 1 // A block on layer1 is every 12s const confirmationTimeStamp = isLayer1 ? modelData.timestamp + 12 * 4 : modelData.timestamp // A block on layer1 is every 12s const finalisationTimeStamp = isLayer1 ? modelData.timestamp + 12 * 64 : modelData.timestamp + 604800 // 7 days in seconds switch(transactionStatus) { case Constants.TransactionStatus.Pending: details += qsTr("Status") + endl details += qsTr("Pending on %1").arg(root.networkName) + endl2 break case Constants.TransactionStatus.Failed: details += qsTr("Status") + endl details += qsTr("Failed on %1").arg(root.networkName) + endl2 break case Constants.TransactionStatus.Verified: { const timestampString = LocaleUtils.formatDateTime(modelData.timestamp * 1000, Locale.LongFormat) details += qsTr("Status") + endl const epoch = parseFloat(Math.abs(walletRootStore.getLatestBlockNumber(modelData.chainId) - rootStore.hex2Dec(modelData.blockNumber)).toFixed(0)).toLocaleString() details += qsTr("Finalised in epoch %1").arg(epoch.toFixed(0)) + endl2 details += qsTr("Signed") + endl + root.timestampString + endl2 details += qsTr("Confirmed") + endl details += LocaleUtils.formatDateTime(confirmationTimeStamp * 1000, Locale.LongFormat) + endl2 break } case Constants.TransactionStatus.Finished: { const timestampString = LocaleUtils.formatDateTime(modelData.timestamp * 1000, Locale.LongFormat) details += qsTr("Status") + endl const epoch = Math.abs(walletRootStore.getLatestBlockNumber(modelData.chainId) - rootStore.hex2Dec(modelData.blockNumber)) details += qsTr("Finalised in epoch %1").arg(epoch.toFixed(0)) + endl2 details += qsTr("Signed") + endl + timestampString + endl2 details += qsTr("Confirmed") + endl details += LocaleUtils.formatDateTime(confirmationTimeStamp * 1000, Locale.LongFormat) + endl2 details += qsTr("Finalised") + endl details += LocaleUtils.formatDateTime(finalisationTimeStamp * 1000, Locale.LongFormat) + endl2 break } default: break } // SUMMARY ADRESSES switch (type) { case Constants.TransactionType.Swap: details += qsTr("From") + endl + root.symbol + endl2 details += qsTr("To") + endl + root.swapSymbol + endl2 details += qsTr("In") + endl + root.fromAddress + endl2 break case Constants.TransactionType.Bridge: details += qsTr("From") + endl + root.networkName + endl2 details += qsTr("To") + endl + root.bridgeNetworkName + endl2 details += qsTr("In") + endl + modelData.from + endl2 break default: details += qsTr("From") + endl + modelData.from + endl2 details += qsTr("To") + endl + modelData.to + endl2 break } const protocolName = "" // TODO fill protocol name for Bridge and Swap if (!!protocolName) { details += qsTr("Using") + endl + protocolName + endl2 } if (!!modelData.txHash) { details += qsTr("%1 Tx hash").arg(root.networkName) + endl + modelData.txHash + endl2 } const bridgeTxHash = "" // TODO fill tx hash for Bridge if (!!bridgeTxHash) { details += qsTr("%1 Tx hash").arg(root.bridgeNetworkName) + endl + bridgeTxHash + endl2 } const protocolFromContractAddress = "" // TODO fill protocol contract address for 'from' network for Bridge and Swap if (!!protocolName && !!protocolFromContractAddress) { details += qsTr("%1 %2 contract address").arg(root.networkName).arg(protocolName) + endl details += protocolFromContractAddress + endl2 } if (!!modelData.contract) { details += qsTr("%1 %2 contract address").arg(root.networkName).arg(root.symbol) + endl details += modelData.contract + endl2 } const protocolToContractAddress = "" // TODO fill protocol contract address for 'to' network for Bridge if (!!protocolToContractAddress && !!protocolName) { details += qsTr("%1 %2 contract address").arg(root.bridgeNetworkName).arg(protocolName) + endl details += protocolToContractAddress + endl2 } const swapContractAddress = "" // TODO fill swap contract address for Swap const bridgeContractAddress = "" // TODO fill token's contract address for 'to' network for Bridge switch (type) { case Constants.TransactionType.Swap: if (!!swapContractAddress) { details += qsTr("%1 %2 contract address").arg(root.networkName).arg(root.swapSymbol) + endl details += swapContractAddress + endl2 } break case Constants.TransactionType.Bridge: if (!!bridgeContractAddress) { details += qsTr("%1 %2 contract address").arg(root.bridgeNetworkName).arg(root.symbol) + endl details += bridgeContractAddress + endl2 } break default: break } // SUMMARY DATA if (type !== Constants.TransactionType.Bridge) { details += qsTr("Network") + endl + networkName + endl2 } details += qsTr("Token format") + endl + modelData.type.toUpperCase() + endl2 details += qsTr("Nonce") + endl + rootStore.hex2Dec(modelData.nonce) + endl2 if (type === Constants.TransactionType.Bridge) { details += qsTr("Included in Block on %1").arg(networkName) + endl details += rootStore.hex2Dec(modelData.blockNumber) + endl2 details += qsTr("Included in Block on %1").arg(bridgeNetworkName) + endl const bridgeBlockNumber = 0 // TODO fill when bridge data is implemented details += rootStore.hex2Dec(bridgeBlockNumber) + endl2 } else { details += qsTr("Included in Block") + endl + rootStore.hex2Dec(modelData.blockNumber) + endl2 } // VALUES const fiatTransactionValue = rootStore.formatCurrencyAmount(root.fiatValue, root.currentCurrency) const feeFiatValue = rootStore.getFiatValue(feeEthValue, "ETH", root.currentCurrency) let valuesString = "" if (!root.isNFT) { switch(type) { case Constants.TransactionType.Send: case Constants.TransactionType.Swap: case Constants.TransactionType.Bridge: valuesString += qsTr("Amount sent %1 (%2)").arg(root.transactionValue).arg(fiatTransactionValue) + endl2 break default: break } if (type === Constants.TransactionType.Swap) { const crypto = rootStore.formatCurrencyAmount(root.swapCryptoValue, root.swapSymbol) const fiat = rootStore.formatCurrencyAmount(root.swapCryptoValue, root.swapSymbol) valuesString += qsTr("Amount received %1 (%2)").arg(crypto).arg(fiat) + endl2 } else if (type === Constants.TransactionType.Bridge) { // Reduce crypto value by fee value const valueInCrypto = rootStore.getCryptoValue(root.fiatValue - feeFiatValue, root.symbol, root.currentCurrency) const crypto = rootStore.formatCurrencyAmount(valueInCrypto, root.symbol) const fiat = rootStore.formatCurrencyAmount(root.fiatValue - feeFiatValue, root.currentCurrency) valuesString += qsTr("Amount received %1 (%2)").arg(crypto).arg(fiat) + endl2 } switch(type) { case Constants.TransactionType.Send: case Constants.TransactionType.Swap: case Constants.TransactionType.Bridge: const feeValue = LocaleUtils.currencyAmountToLocaleString(modelData.totalFees) const feeFiat = rootStore.formatCurrencyAmount(feeFiatValue, root.currentCurrency) valuesString += qsTr("Fees %1 (%2)").arg(feeValue).arg(feeFiat) + endl2 break default: break } } if (!root.isNFT || type !== Constants.TransactionType.Receive) { if (type === Constants.TransactionType.Destroy || root.isNFT) { const feeCrypto = rootStore.formatCurrencyAmount(feeEthValue, "ETH") const feeFiat = rootStore.formatCurrencyAmount(feeFiatValue, root.currentCurrency) valuesString += qsTr("Fees %1 (%2)").arg(feeCrypto).arg(feeFiat) + endl2 } else if (type === Constants.TransactionType.Receive || (type === Constants.TransactionType.Buy && isLayer1)) { valuesString += qsTr("Total %1 (%2)").arg(root.transactionValue).arg(fiatTransactionValue) + endl2 } else { const feeEth = rootStore.formatCurrencyAmount(feeEthValue, "ETH") valuesString += qsTr("Total %1 + %2 (%3)").arg(root.transactionValue).arg(feeEth).arg(fiatTransactionValue) + endl2 } } if (valuesString !== "") { const timestampString = LocaleUtils.formatDateTime(modelData.timestamp * 1000, Locale.LongFormat) details += qsTr("Values at %1").arg(timestampString) + endl2 details += valuesString + endl2 } // Remove locale specific number separator details = details.replace(/\ /, ' ') // Remove empty new lines at the end return details.replace(/[\r\n\s]*$/, '') } rightPadding: 16 enabled: !loading color: sensor.containsMouse ? Theme.palette.baseColor5 : Style.current.transparent statusListItemIcon.active: (loading || root.asset.name) asset { width: 24 height: 24 isImage: false imgIsIdenticon: true isLetterIdenticon: loading name: { if (!root.isModelDataValid) return "" switch(modelData.txType) { case Constants.TransactionType.Send: return "send" case Constants.TransactionType.Receive: return "receive" case Constants.TransactionType.Buy: case Constants.TransactionType.Sell: return "token" case Constants.TransactionType.Destroy: return "destroy" case Constants.TransactionType.Swap: return "swap" case Constants.TransactionType.Bridge: return "bridge" default: return "" } } bgColor: "transparent" color: Theme.palette.directColor1 bgBorderWidth: 1 bgBorderColor: Theme.palette.primaryColor3 } sensor.children: [ StatusRoundIcon { id: leftIconStatusIcon visible: !root.loading anchors { right: root.statusListItemIcon.right bottom: root.statusListItemIcon.bottom } asset: root.statusIconAsset } ] // Title title: { if (root.loading) { return "dummmy" } else if (!root.isModelDataValid) { return "" } const isPending = root.transactionStatus === Constants.TransactionStatus.Pending const failed = root.transactionStatus === Constants.TransactionStatus.Failed switch(modelData.txType) { case Constants.TransactionType.Send: return failed ? qsTr("Send failed") : (isPending ? qsTr("Sending") : qsTr("Sent")) case Constants.TransactionType.Receive: return failed ? qsTr("Receive failed") : (isPending ? qsTr("Receiving") : qsTr("Received")) case Constants.TransactionType.Buy: return failed ? qsTr("Buy failed") : (isPending ? qsTr("Buying") : qsTr("Bought")) case Constants.TransactionType.Sell: return failed ? qsTr("Sell failed") : (isPending ? qsTr("Selling") : qsTr("Sold")) case Constants.TransactionType.Destroy: return failed ? qsTr("Destroy failed") : (isPending ? qsTr("Destroying") : qsTr("Destroyed")) case Constants.TransactionType.Swap: return failed ? qsTr("Swap failed") : (isPending ? qsTr("Swapping") : qsTr("Swapped")) case Constants.TransactionType.Bridge: return failed ? qsTr("Bridge failed") : (isPending ? qsTr("Bridging") : qsTr("Bridged")) default: return "" } } statusListItemTitleArea.anchors.rightMargin: root.rightPadding statusListItemTitle.font.weight: Font.DemiBold statusListItemTitle.font.pixelSize: root.loading ? d.loadingPixelSize : d.titlePixelSize // title icons and date statusListItemTitleIcons.sourceComponent: Row { spacing: 8 Row { visible: !root.loading spacing: swapTokenImage.visible ? -tokenImage.width * 0.2 : 0 StatusRoundIcon { id: tokenImage anchors.verticalCenter: parent.verticalCenter asset: root.tokenIconAsset } StatusRoundIcon { id: swapTokenImage visible: root.isModelDataValid && !root.isNFT && !!root.swapTokenImage &&modelData.txType === Constants.TransactionType.Swap anchors.verticalCenter: parent.verticalCenter asset: StatusAssetSettings { width: root.tokenIconAsset.width height: root.tokenIconAsset.height bgWidth: width + 2 bgHeight: height + 2 bgRadius: bgWidth / 2 bgColor: root.color isImage:root.tokenIconAsset.isImage color: root.tokenIconAsset.color name: root.swapTokenImage isLetterIdenticon: root.tokenIconAsset.isLetterIdenticon } } } StatusTextWithLoadingState { anchors.verticalCenter: parent.verticalCenter text: root.loading ? root.title : root.timeStampText verticalAlignment: Qt.AlignVCenter font.pixelSize: root.loading ? d.loadingPixelSize : d.datePixelSize visible: !!text loading: root.loading customColor: Theme.palette.baseColor1 } } // subtitle subTitle: { if (!root.isModelDataValid) { return "" } switch(modelData.txType) { case Constants.TransactionType.Receive: return qsTr("%1 from %2 via %3").arg(transactionValue).arg(fromAddress).arg(networkName) case Constants.TransactionType.Buy: case Constants.TransactionType.Sell: return qsTr("%1 on %2 via %3").arg(transactionValue).arg(toAddress).arg(networkName) case Constants.TransactionType.Destroy: return qsTr("%1 at %2 via %3").arg(transactionValue).arg(toAddress).arg(networkName) case Constants.TransactionType.Swap: return qsTr("%1 to %2 via %3").arg(transactionValue).arg(swapTransactionValue).arg(networkName) case Constants.TransactionType.Bridge: return qsTr("%1 from %2 to %3").arg(transactionValue).arg(bridgeNetworkName).arg(networkName) default: return qsTr("%1 to %2 via %3").arg(transactionValue).arg(toAddress).arg(networkName) } } statusListItemSubTitle.maximumLoadingStateWidth: 300 statusListItemSubTitle.customColor: Theme.palette.directColor1 statusListItemSubTitle.font.pixelSize: root.loading ? d.loadingPixelSize : d.subtitlePixelSize statusListItemTagsRowLayout.anchors.topMargin: 4 // Spacing between title row nad subtitle row // Right side components components: [ Loader { active: !headerStatusLoader.active visible: active sourceComponent: ColumnLayout { StatusTextWithLoadingState { id: cryptoValueText text: { if (root.loading) { return "dummy text" } else if (!root.isModelDataValid || root.isNFT) { return "" } switch(modelData.txType) { case Constants.TransactionType.Send: case Constants.TransactionType.Sell: return "−" + root.transactionValue case Constants.TransactionType.Buy: case Constants.TransactionType.Receive: return "+" + root.transactionValue case Constants.TransactionType.Swap: return "-%2 / +%5" .arg(Theme.palette.directColor1) .arg(root.transactionValue) .arg(Theme.palette.baseColor1) .arg(Theme.palette.successColor1) .arg(root.swapTransactionValue) case Constants.TransactionType.Bridge: return "−" + root.rootStore.formatCurrencyAmount(feeCryptoValue, root.symbol) default: return "" } } horizontalAlignment: Qt.AlignRight Layout.alignment: Qt.AlignRight font.pixelSize: root.loading ? d.loadingPixelSize : 13 customColor: { if (!root.isModelDataValid) return "" switch(modelData.txType) { case Constants.TransactionType.Receive: case Constants.TransactionType.Buy: case Constants.TransactionType.Swap: return Theme.palette.successColor1 default: return Theme.palette.directColor1 } } loading: root.loading } StatusTextWithLoadingState { id: fiatValueText Layout.alignment: Qt.AlignRight horizontalAlignment: Qt.AlignRight text: { if (root.loading) { return "dummy text" } else if (!root.isModelDataValid || root.isNFT) { return "" } switch(modelData.txType) { case Constants.TransactionType.Send: case Constants.TransactionType.Sell: case Constants.TransactionType.Buy: return "−" + root.rootStore.formatCurrencyAmount(root.fiatValue, root.currentCurrency) case Constants.TransactionType.Receive: return "+" + root.rootStore.formatCurrencyAmount(root.fiatValue, root.currentCurrency) case Constants.TransactionType.Swap: return "-%1 / +%2".arg(root.rootStore.formatCurrencyAmount(root.fiatValue, root.currentCurrency)) .arg(root.rootStore.formatCurrencyAmount(root.swapFiatValue, root.currentCurrency)) case Constants.TransactionType.Bridge: return "−" + root.rootStore.formatCurrencyAmount(root.feeFiatValue, root.currentCurrency) default: return "" } } font.pixelSize: root.loading ? d.loadingPixelSize : 12 customColor: Theme.palette.baseColor1 loading: root.loading } } }, Loader { id: headerStatusLoader active: false visible: active sourceComponent: Rectangle { id: statusRect width: transactionTypeIcon.width + (retryButton.visible ? retryButton.width + 5 : 0) height: transactionTypeIcon.height anchors.verticalCenter: parent.verticalCenter color: "transparent" radius: 100 border { width: retryButton.visible ? 1 : 0 color: root.asset.bgBorderColor } StatusButton { id: retryButton anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 10 radius: height / 2 height: parent.height * 0.7 verticalPadding: 0 horizontalPadding: radius text: qsTr("Retry") size: StatusButton.Small type: StatusButton.Primary visible: !root.loading && root.transactionStatus === Constants.TransactionStatus.Failed onClicked: root.retryClicked() } StatusSmartIdenticon { id: transactionTypeIcon anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right enabled: false asset: root.asset active: !!root.asset.name loading: root.loading name: root.title } StatusRoundIcon { visible: !root.loading anchors { right: transactionTypeIcon.right bottom: transactionTypeIcon.bottom } asset: root.statusIconAsset } } } ] states: [ State { name: "header" PropertyChanges { target: headerStatusLoader active: true } PropertyChanges { target: leftIconStatusIcon visible: false } PropertyChanges { target: root.statusListItemIcon active: false } PropertyChanges { target: root.asset bgBorderWidth: root.transactionStatus === Constants.TransactionStatus.Failed ? 0 : 1 width: 34 height: 34 bgWidth: 56 bgHeight: 56 } PropertyChanges { target: root.statusIconAsset width: 17 height: 17 } PropertyChanges { target: root.tokenIconAsset width: 20 height: 20 } PropertyChanges { target: d titlePixelSize: 17 datePixelSize: 13 subtitlePixelSize: 15 loadingPixelSize: 14 } } ] }