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" title: "SignTokenTransactionsPopup"
section: "Popups" section: "Popups"
} }
ListElement {
title: "SignMultiTokenTransactionsPopup"
section: "Popups"
}
ListElement { ListElement {
title: "RemotelyDestructPopup" title: "RemotelyDestructPopup"
section: "Popups" 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 return true
} }
function roleNames(model) {
return Internal.ModelUtils.roleNames(model)
}
} }

View File

@ -19,11 +19,17 @@ SettingsPageLayout {
required property var membersModel 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 property int viewWidth: 560 // by design
signal airdropClicked(var airdropTokens, var addresses, var membersPubKeys) signal airdropClicked(var airdropTokens, var addresses, var membersPubKeys)
signal navigateToMintTokenSettings signal airdropFeesRequested(var contractKeysAndAmounts, var addresses)
signal navigateToMintTokenSettings
function navigateBack() { function navigateBack() {
stackManager.pop(StackView.Immediate) stackManager.pop(StackView.Immediate)
@ -109,6 +115,10 @@ SettingsPageLayout {
collectiblesModel: root.collectiblesModel collectiblesModel: root.collectiblesModel
membersModel: root.membersModel membersModel: root.membersModel
Binding on airdropFees {
value: root.airdropFees
}
onAirdropClicked: { onAirdropClicked: {
root.airdropClicked(airdropTokens, addresses, membersPubKeys) root.airdropClicked(airdropTokens, addresses, membersPubKeys)
stackManager.clear(d.welcomeViewState, StackView.Immediate) stackManager.clear(d.welcomeViewState, StackView.Immediate)
@ -118,6 +128,7 @@ SettingsPageLayout {
Component.onCompleted: { Component.onCompleted: {
d.selectToken.connect(view.selectToken) d.selectToken.connect(view.selectToken)
d.addAddresses.connect(view.addAddresses) 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 AlertPopup 1.0 AlertPopup.qml
BurnTokensPopup 1.0 BurnTokensPopup.qml BurnTokensPopup 1.0 BurnTokensPopup.qml
CreateChannelPopup 1.0 CreateChannelPopup.qml
CommunityTokenPermissionsPopup 1.0 CommunityTokenPermissionsPopup.qml CommunityTokenPermissionsPopup 1.0 CommunityTokenPermissionsPopup.qml
CreateChannelPopup 1.0 CreateChannelPopup.qml
RemotelyDestructPopup 1.0 RemotelyDestructPopup.qml RemotelyDestructPopup 1.0 RemotelyDestructPopup.qml
SignMultiTokenTransactionsPopup 1.0 SignMultiTokenTransactionsPopup.qml
SignTokenTransactionsPopup 1.0 SignTokenTransactionsPopup.qml SignTokenTransactionsPopup 1.0 SignTokenTransactionsPopup.qml

View File

@ -447,6 +447,10 @@ StatusSectionLayout {
onAirdropClicked: communityTokensStore.airdrop(root.community.id, airdropTokens, addresses) onAirdropClicked: communityTokensStore.airdrop(root.community.id, airdropTokens, addresses)
onNavigateToMintTokenSettings: root.goTo(Constants.CommunitySettingsSections.MintTokens) onNavigateToMintTokenSettings: root.goTo(Constants.CommunitySettingsSections.MintTokens)
onAirdropFeesRequested:
communityTokensStore.computeAirdropFee(
root.community.id, contractKeysAndAmounts, addresses)
Connections { Connections {
target: mintPanel target: mintPanel
@ -461,6 +465,14 @@ StatusSectionLayout {
airdropPanel.addAddresses(addresses) airdropPanel.addAddresses(addresses)
} }
} }
Connections {
target: rootStore.communityTokensStore
function onAirdropFeeUpdated(airdropFees) {
airdropPanel.airdropFees = airdropFees
}
}
} }
onCurrentIndexChanged: root.backButtonName = centerPanelContentLoader.item.children[d.currentIndex].previousPageName 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 utils 1.0
import shared.panels 1.0 import shared.panels 1.0
import AppLayouts.Chat.controls.community 1.0
import AppLayouts.Chat.helpers 1.0 import AppLayouts.Chat.helpers 1.0
import AppLayouts.Chat.panels.communities 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 import SortFilterProxyModel 0.2
@ -26,6 +27,23 @@ StatusScrollView {
// Community members model: // Community members model:
required property var membersModel 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 property int viewWidth: 560 // by design
readonly property var selectedHoldingsModel: ListModel {} readonly property var selectedHoldingsModel: ListModel {}
@ -35,6 +53,9 @@ StatusScrollView {
airdropRecipientsSelector.valid airdropRecipientsSelector.valid
signal airdropClicked(var airdropTokens, var addresses, var membersPubKeys) signal airdropClicked(var airdropTokens, var addresses, var membersPubKeys)
signal airdropFeesRequested(var contractKeysAndAmounts, var addresses)
signal navigateToMintTokenSettings signal navigateToMintTokenSettings
function selectToken(key, amount, type) { function selectToken(key, amount, type) {
@ -81,6 +102,7 @@ StatusScrollView {
supply: modelItem.supply, supply: modelItem.supply,
infiniteSupply: modelItem.infiniteSupply, infiniteSupply: modelItem.infiniteSupply,
contractUniqueKey: modelItem.contractUniqueKey, contractUniqueKey: modelItem.contractUniqueKey,
accountName: modelItem.accountName
} }
} }
} }
@ -401,6 +423,64 @@ StatusScrollView {
enabled: root.isFullyFilled enabled: root.isFullyFilled
onClicked: { 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( const airdropTokens = ModelUtils.modelToArray(
root.selectedHoldingsModel, root.selectedHoldingsModel,
["contractUniqueKey", "amount"]) ["contractUniqueKey", "amount"])
@ -412,6 +492,52 @@ StatusScrollView {
root.airdropClicked(airdropTokens, addresses_, pubKeys) 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 deployFeeUpdated(var ethCurrency, var fiatCurrency, int error)
signal selfDestructFeeUpdated(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) signal deploymentStateChanged(string communityId, int status, string url)
@ -52,19 +53,23 @@ QtObject {
readonly property Connections connections: Connections { readonly property Connections connections: Connections {
target: communityTokensModuleInst target: communityTokensModuleInst
function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode) { function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
root.deployFeeUpdated(ethCurrency, fiatCurrency, errorCode) root.deployFeeUpdated(ethCurrency, fiatCurrency, errorCode)
} }
function onSelfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode) { function onSelfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
root.selfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode) root.selfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode)
} }
function onAirdropFeesUpdated(jsonFees) { function onAirdropFeesUpdated(jsonFees) {
console.log("Fees:", jsonFees) root.airdropFeeUpdated(JSON.parse(jsonFees))
} }
function onDeploymentStateChanged(communityId, status, url) { function onDeploymentStateChanged(communityId, status, url) {
root.deploymentStateChanged(communityId, status, url) root.deploymentStateChanged(communityId, status, url)
} }
function onRemoteDestructStateChanged(communityId, tokenName, status, url) { function onRemoteDestructStateChanged(communityId, tokenName, status, url) {
root.remoteDestructStateChanged(communityId, tokenName, status, url) root.remoteDestructStateChanged(communityId, tokenName, status, url)
} }
@ -99,7 +104,9 @@ QtObject {
communityTokensModuleInst.airdropCollectibles(communityId, JSON.stringify(airdropTokens), JSON.stringify(addresses)) communityTokensModuleInst.airdropCollectibles(communityId, JSON.stringify(airdropTokens), JSON.stringify(addresses))
} }
function computeAirdropFee(communityId, airdropTokens, addresses) { function computeAirdropFee(communityId, contractKeysAndAmounts, addresses) {
communityTokensModuleInst.computeAirdropCollectiblesFee(communityId, JSON.stringify(airdropTokens), JSON.stringify(addresses)) communityTokensModuleInst.computeAirdropCollectiblesFee(
communityId, JSON.stringify(contractKeysAndAmounts),
JSON.stringify(addresses))
} }
} }