feat(CommunityAirdrops): Fees popup with multiple entries for airdrops

Closes: #10484
This commit is contained in:
Michał Cieślak 2023-06-06 17:32:53 +02:00 committed by Michał
parent 38d3b32cb9
commit 8b44e686f4
9 changed files with 498 additions and 21 deletions

View File

@ -193,6 +193,10 @@ ListModel {
title: "SignTokenTransactionsPopup"
section: "Popups"
}
ListElement {
title: "SignMultiTokenTransactionsPopup"
section: "Popups"
}
ListElement {
title: "RemotelyDestructPopup"
section: "Popups"

View File

@ -0,0 +1,133 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Storybook 1.0
import AppLayouts.Chat.popups.community 1.0
SplitView {
Logs { id: logs }
SplitView {
orientation: Qt.Vertical
SplitView.fillWidth: true
Pane {
id: pane
SplitView.fillWidth: true
SplitView.fillHeight: true
PopupBackground {
anchors.fill: parent
}
Button {
anchors.centerIn: parent
text: "Reopen"
onClicked: dialog.open()
}
SignMultiTokenTransactionsPopup {
id: dialog
model: ListModel {
id: feesModel
ListElement {
account: "My Account 1"
network: "Optimism"
symbol: "TAT"
amount: 2
feeText: "0.0015 ($75.43)"
}
ListElement {
account: "My Account 2"
network: "Arbitrum"
symbol: "SNT"
amount: 34
feeText: "0.0085 ETH ($175.43)"
}
}
closePolicy: Popup.NoAutoClose
visible: true
modal: false
destroyOnClose: false
title: `Sign transaction - Airdrop ${model.count} token(s) to 32 recipients`
isFeeLoading: loadingSwitch.checked
showSummary: showSummarySwitch.checked
errorText: errorTextField.text
totalFeeText: "0.01 ETH ($265.43)"
onSignTransactionClicked: logs.logEvent("SignTokenTransactionsPopup::onSignTransactionClicked")
onCancelClicked: logs.logEvent("SignTokenTransactionsPopup::onCancelClicked")
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 150
logsView.logText: logs.logText
}
}
Pane {
SplitView.minimumWidth: 300
SplitView.preferredWidth: 300
ColumnLayout {
anchors.fill: parent
Label {
Layout.fillWidth: true
text: "Error text"
}
TextField {
id: errorTextField
Layout.fillWidth: true
text: ""
}
SpinBox {
id: recipientsCountSpinBox
from: 1
to: 1000
}
Switch {
id: loadingSwitch
text: "Is fee loading"
checked: false
}
Switch {
id: showSummarySwitch
text: "Is summary visible"
checked: true
}
Item {
Layout.fillHeight: true
}
}
}
}

View File

@ -117,4 +117,8 @@ QtObject {
return true
}
function roleNames(model) {
return Internal.ModelUtils.roleNames(model)
}
}

View File

@ -19,11 +19,17 @@ SettingsPageLayout {
required property var membersModel
// JS object specifing fees for the airdrop operation, should be set to
// provide response to airdropFeesRequested signal.
// Refer CommunityNewAirdropView::airdropFees for details.
property var airdropFees: null
property int viewWidth: 560 // by design
signal airdropClicked(var airdropTokens, var addresses, var membersPubKeys)
signal navigateToMintTokenSettings
signal airdropFeesRequested(var contractKeysAndAmounts, var addresses)
signal navigateToMintTokenSettings
function navigateBack() {
stackManager.pop(StackView.Immediate)
@ -109,6 +115,10 @@ SettingsPageLayout {
collectiblesModel: root.collectiblesModel
membersModel: root.membersModel
Binding on airdropFees {
value: root.airdropFees
}
onAirdropClicked: {
root.airdropClicked(airdropTokens, addresses, membersPubKeys)
stackManager.clear(d.welcomeViewState, StackView.Immediate)
@ -118,6 +128,7 @@ SettingsPageLayout {
Component.onCompleted: {
d.selectToken.connect(view.selectToken)
d.addAddresses.connect(view.addAddresses)
airdropFeesRequested.connect(root.airdropFeesRequested)
}
}
}

View File

@ -0,0 +1,179 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
StatusDialog {
id: root
// account, amount, symbol, network, feeText
property alias model: repeater.model
property alias showSummary: summaryRow.visible
property alias errorText: errorTxt.text
property alias totalFeeText: totalFeeText.text
property bool isFeeLoading
signal signTransactionClicked()
signal cancelClicked()
QtObject {
id: d
property int minTextWidth: 50
}
implicitWidth: 600 // by design
topPadding: 2 * Style.current.padding // by design
bottomPadding: topPadding
contentItem: ColumnLayout {
id: column
spacing: Style.current.padding
Repeater {
id: repeater
Item {
Layout.fillWidth: true
implicitHeight: delegateColumn.implicitHeight
ColumnLayout {
id: delegateColumn
width: parent.width
RowLayout {
Layout.fillWidth: true
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Airdropping %1 %2 on %3")
.arg(model.amount).arg(model.symbol)
.arg(model.network)
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
}
StatusDotsLoadingIndicator {
visible: root.isFeeLoading
Layout.rightMargin: Style.current.padding
}
StatusBaseText {
text: model.feeText
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
color: Theme.palette.baseColor1
visible: !root.isFeeLoading
}
}
StatusBaseText {
Layout.fillWidth: true
text: qsTr("via %1").arg(model.account)
horizontalAlignment: Text.AlignLeft
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
color: Theme.palette.baseColor1
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Theme.palette.baseColor2
}
RowLayout {
id: summaryRow
Layout.fillHeight: false
Layout.fillWidth: true
Layout.topMargin: Style.current.halfPadding
Layout.bottomMargin: -Style.current.halfPadding
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Total")
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
}
StatusDotsLoadingIndicator {
visible: root.isFeeLoading
Layout.rightMargin: Style.current.padding
}
StatusBaseText {
id: totalFeeText
font.pixelSize: Style.current.primaryTextFontSize
visible: !root.isFeeLoading
}
}
StatusBaseText {
id: errorTxt
Layout.topMargin: Style.current.halfPadding
Layout.bottomMargin: -Style.current.halfPadding
Layout.fillWidth: true
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignRight
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.dangerColor1
text: root.errorText
visible: root.errorText !== ""
}
}
footer: StatusDialogFooter {
spacing: Style.current.padding
rightButtons: ObjectModel {
StatusButton {
text: qsTr("Cancel")
type: StatusBaseButton.Type.Danger
onClicked: {
root.cancelClicked()
root.close()
}
}
StatusButton {
enabled: root.errorText === "" && !root.isFeeLoading
icon.name: "password"
text: qsTr("Sign transaction")
onClicked: {
root.signTransactionClicked()
root.close()
}
}
}
}
}

View File

@ -1,6 +1,7 @@
AlertPopup 1.0 AlertPopup.qml
BurnTokensPopup 1.0 BurnTokensPopup.qml
CreateChannelPopup 1.0 CreateChannelPopup.qml
CommunityTokenPermissionsPopup 1.0 CommunityTokenPermissionsPopup.qml
CreateChannelPopup 1.0 CreateChannelPopup.qml
RemotelyDestructPopup 1.0 RemotelyDestructPopup.qml
SignMultiTokenTransactionsPopup 1.0 SignMultiTokenTransactionsPopup.qml
SignTokenTransactionsPopup 1.0 SignTokenTransactionsPopup.qml

View File

@ -447,6 +447,10 @@ StatusSectionLayout {
onAirdropClicked: communityTokensStore.airdrop(root.community.id, airdropTokens, addresses)
onNavigateToMintTokenSettings: root.goTo(Constants.CommunitySettingsSections.MintTokens)
onAirdropFeesRequested:
communityTokensStore.computeAirdropFee(
root.community.id, contractKeysAndAmounts, addresses)
Connections {
target: mintPanel
@ -461,6 +465,14 @@ StatusSectionLayout {
airdropPanel.addAddresses(addresses)
}
}
Connections {
target: rootStore.communityTokensStore
function onAirdropFeeUpdated(airdropFees) {
airdropPanel.airdropFees = airdropFees
}
}
}
onCurrentIndexChanged: root.backButtonName = centerPanelContentLoader.item.children[d.currentIndex].previousPageName

View File

@ -9,9 +9,10 @@ import StatusQ.Core.Utils 0.1
import utils 1.0
import shared.panels 1.0
import AppLayouts.Chat.controls.community 1.0
import AppLayouts.Chat.helpers 1.0
import AppLayouts.Chat.panels.communities 1.0
import AppLayouts.Chat.controls.community 1.0
import AppLayouts.Chat.popups.community 1.0
import SortFilterProxyModel 0.2
@ -26,6 +27,23 @@ StatusScrollView {
// Community members model:
required property var membersModel
// JS object specifing fees for the airdrop operation, should be set to
// provide response to airdropFeesRequested signal.
//
// The expected structure is as follows:
// {
// fees: [{
// ethFee: {CurrencyAmount JSON},
// fiatFee: {CurrencyAmount JSON},
// contractUniqueKey: string,
// errorCode: ComputeFeeErrorCode (int)
// }],
// totalEthFee: {CurrencyAmount JSON},
// totalFiatFee: {CurrencyAmount JSON},
// errorCode: ComputeFeeErrorCode (int)
// }
property var airdropFees: null
property int viewWidth: 560 // by design
readonly property var selectedHoldingsModel: ListModel {}
@ -35,6 +53,9 @@ StatusScrollView {
airdropRecipientsSelector.valid
signal airdropClicked(var airdropTokens, var addresses, var membersPubKeys)
signal airdropFeesRequested(var contractKeysAndAmounts, var addresses)
signal navigateToMintTokenSettings
function selectToken(key, amount, type) {
@ -81,6 +102,7 @@ StatusScrollView {
supply: modelItem.supply,
infiniteSupply: modelItem.infiniteSupply,
contractUniqueKey: modelItem.contractUniqueKey,
accountName: modelItem.accountName
}
}
}
@ -401,6 +423,64 @@ StatusScrollView {
enabled: root.isFullyFilled
onClicked: {
feesPopup.open()
}
}
SignMultiTokenTransactionsPopup {
id: feesPopup
destroyOnClose: false
model: ListModel {
id: feesModel
}
isFeeLoading: root.airdropFees === null ||
(root.airdropFees.errorCode !== Constants.ComputeFeeErrorCode.Success &&
root.airdropFees.errorCode !== Constants.ComputeFeeErrorCode.Balance)
onOpened: {
const title1 = qsTr("Sign transaction - Airdrop %n token(s)", "",
selectedHoldingsModel.rowCount())
const title2 = qsTr("to %n recipient(s)", "",
addresses.count + airdropRecipientsSelector.membersModel.count)
title = `${title1} ${title2}`
root.airdropFees = null
errorText = ""
feesModel.clear()
const airdropTokens = ModelUtils.modelToArray(
root.selectedHoldingsModel,
["contractUniqueKey", "accountName",
"key", "amount", "tokenText",
"networkText"])
airdropTokens.forEach(entry => {
feesModel.append({
contractUniqueKey: entry.contractUniqueKey,
key: entry.key,
amount: entry.amount,
account: entry.accountName,
symbol: entry.key,
network: entry.networkText,
feeText: ""
})
})
const contractKeysAndAmounts = airdropTokens.map(item => ({
amount: item.amount,
contractUniqueKey: item.contractUniqueKey
}))
const addresses_ = ModelUtils.modelToArray(
addresses, ["address"]).map(e => e.address)
airdropFeesRequested(contractKeysAndAmounts, addresses_)
}
onSignTransactionClicked: {
const airdropTokens = ModelUtils.modelToArray(
root.selectedHoldingsModel,
["contractUniqueKey", "amount"])
@ -412,6 +492,52 @@ StatusScrollView {
root.airdropClicked(airdropTokens, addresses_, pubKeys)
}
Connections {
target: root
function onAirdropFeesChanged() {
if (root.airdropFees === null)
return
const fees = root.airdropFees.fees
const errorCode = root.airdropFees.errorCode
function buildFeeString(ethFee, fiatFee) {
return `${LocaleUtils.currencyAmountToLocaleString(ethFee)} (${LocaleUtils.currencyAmountToLocaleString(fiatFee)})`
}
if (errorCode === Constants.ComputeFeeErrorCode.Infura) {
feesPopup.errorText = qsTr("Infura error")
return
}
if (errorCode === Constants.ComputeFeeErrorCode.Success ||
errorCode === Constants.ComputeFeeErrorCode.Balance) {
fees.forEach(fee => {
const idx = ModelUtils.indexOf(
feesModel, "contractUniqueKey",
fee.contractUniqueKey)
feesPopup.model.set(idx, {
feeText: buildFeeString(fee.ethFee, fee.fiatFee)
})
})
feesPopup.totalFeeText = buildFeeString(
root.airdropFees.totalEthFee,
root.airdropFees.totalFiatFee)
if (errorCode === Constants.ComputeFeeErrorCode.Balance) {
feesPopup.errorText = qsTr("Not enough funds to make transaction")
}
return
}
feesPopup.errorText = qsTr("Unknown error")
}
}
}
}
}

View File

@ -15,6 +15,7 @@ QtObject {
signal deployFeeUpdated(var ethCurrency, var fiatCurrency, int error)
signal selfDestructFeeUpdated(var ethCurrency, var fiatCurrency, int error)
signal airdropFeeUpdated(var airdropFees)
signal deploymentStateChanged(string communityId, int status, string url)
@ -51,23 +52,27 @@ QtObject {
}
readonly property Connections connections: Connections {
target: communityTokensModuleInst
function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
root.deployFeeUpdated(ethCurrency, fiatCurrency, errorCode)
}
function onSelfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
root.selfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode)
}
function onAirdropFeesUpdated(jsonFees) {
console.log("Fees:", jsonFees)
}
target: communityTokensModuleInst
function onDeploymentStateChanged(communityId, status, url) {
root.deploymentStateChanged(communityId, status, url)
}
function onRemoteDestructStateChanged(communityId, tokenName, status, url) {
root.remoteDestructStateChanged(communityId, tokenName, status, url)
}
function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
root.deployFeeUpdated(ethCurrency, fiatCurrency, errorCode)
}
function onSelfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
root.selfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode)
}
function onAirdropFeesUpdated(jsonFees) {
root.airdropFeeUpdated(JSON.parse(jsonFees))
}
function onDeploymentStateChanged(communityId, status, url) {
root.deploymentStateChanged(communityId, status, url)
}
function onRemoteDestructStateChanged(communityId, tokenName, status, url) {
root.remoteDestructStateChanged(communityId, tokenName, status, url)
}
}
function computeDeployFee(chainId, accountAddress) {
@ -99,7 +104,9 @@ QtObject {
communityTokensModuleInst.airdropCollectibles(communityId, JSON.stringify(airdropTokens), JSON.stringify(addresses))
}
function computeAirdropFee(communityId, airdropTokens, addresses) {
communityTokensModuleInst.computeAirdropCollectiblesFee(communityId, JSON.stringify(airdropTokens), JSON.stringify(addresses))
function computeAirdropFee(communityId, contractKeysAndAmounts, addresses) {
communityTokensModuleInst.computeAirdropCollectiblesFee(
communityId, JSON.stringify(contractKeysAndAmounts),
JSON.stringify(addresses))
}
}