fix(Update fees periodically): Update airdrop flows to use fees subscriber

This commit is contained in:
Alex Jbanca 2023-09-01 07:28:50 +03:00 committed by Alex Jbanca
parent f2b3ba1ae7
commit f9e7265447
5 changed files with 182 additions and 187 deletions

View File

@ -104,8 +104,6 @@ SplitView {
assetsModel: AssetsModel {} assetsModel: AssetsModel {}
collectiblesModel: ListModel {} collectiblesModel: ListModel {}
accountsModel: ListModel {}
CollectiblesModel { CollectiblesModel {
id: collectiblesModel id: collectiblesModel
} }

View File

@ -99,10 +99,7 @@ SplitView {
interval: 2000 interval: 2000
property var response property var feesPerContract: []
function requestMockedFees(contractKeysAndAmounts) {
const fees = []
function createAmount(amount, symbol, decimals) { function createAmount(amount, symbol, decimals) {
return { return {
@ -111,20 +108,26 @@ SplitView {
} }
} }
function requestMockedFees(contractKeysAndAmounts) {
if (!loader.item)
return
const view = loader.item
view.feesAvailable = false
view.totalFeeText = ""
view.feeErrorText = ""
view.feesPerSelectedContract = []
const fees = []
contractKeysAndAmounts.forEach(entry => { contractKeysAndAmounts.forEach(entry => {
fees.push({ fees.push({
ethFee: createAmount(0.0002120115, "ETH", 4), contractUniqueKey: entry.contractUniqueKey,
fiatFee: createAmount(123.15, "USD", 2), feeText: "0.0002120115 ETH (123.15 USD)"
errorCode: 0,
contractUniqueKey: entry.contractUniqueKey
}) })
}) })
response = { feesPerContract = fees
fees, errorCode: feesErrorsButtonGroup.checkedButton.code,
totalEthFee: createAmount(0.0002120115 * fees.length, "ETH", 4),
totalFiatFee: createAmount(123.15 * fees.length, "USD", 2)
}
restart() restart()
} }
@ -134,7 +137,10 @@ SplitView {
return return
const view = loader.item const view = loader.item
view.airdropFees = response view.totalFeeText = createAmount(0.0002120115 * feesPerContract.length, "ETH", 4) + "(" ,createAmount(123.15 * feesPerContract.length, "USD", 2),"USD)"
view.feeErrorText = feesErrorsButtonGroup.checkedButton.code ? feesErrorsButtonGroup.checkedButton.text : ""
view.feesAvailable = true
view.feesPerSelectedContract = feesCalculationTimer.feesPerContract
} }
} }
@ -252,6 +258,14 @@ SplitView {
assetsModel: AssetsModel {} assetsModel: AssetsModel {}
collectiblesModel: CollectiblesModel {} collectiblesModel: CollectiblesModel {}
membersModel: members membersModel: members
totalFeeText: ""
feeErrorText: ""
feesPerSelectedContract: []
feesAvailable: false
onShowingFeesChanged: {
feesCalculationTimer.requestMockedFees(loader.item.selectedContractKeysAndAmounts)
}
accountsModel: ListModel { accountsModel: ListModel {
ListElement { ListElement {
@ -283,15 +297,6 @@ SplitView {
"membersPubKeys", "feeAccountAddress"], "membersPubKeys", "feeAccountAddress"],
arguments) arguments)
} }
onAirdropFeesRequested: {
logs.logEvent("EditAirdropView::airdropFeesRequested",
["contractKeysAndAmounts", "addresses",
"feeAccountAddress"],
arguments)
feesCalculationTimer.requestMockedFees(contractKeysAndAmounts)
}
} }
} }
} }
@ -317,6 +322,12 @@ SplitView {
id: feesErrorsButtonGroup id: feesErrorsButtonGroup
buttons: feesErrorsRow.children buttons: feesErrorsRow.children
onCheckedButtonChanged: {
if(!loader.item)
return
feesCalculationTimer.requestMockedFees(loader.item.selectedContractKeysAndAmounts)
}
} }
RowLayout { RowLayout {

View File

@ -0,0 +1,70 @@
import QtQuick 2.15
import StatusQ.Core 0.1
import utils 1.0
/*!
\qmltype AirdropFeesSubscriber
\inherits QtObject
\brief Helper object that holds the request data and the fee response when available
*/
QtObject {
id: root
required property string communityId
required property var contractKeysAndAmounts
required property var addressesToAirdrop
required property string feeAccountAddress
required property bool enabled
// 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 airdropFeesResponse: null
readonly property string feesError: {
if (!airdropFeesResponse) return ""
if (airdropFeesResponse.errorCode === Constants.ComputeFeeErrorCode.Success) return ""
if (airdropFeesResponse.errorCode === Constants.ComputeFeeErrorCode.Balance)
return qsTr("Your account does not have enough ETH to pay the gas fee for this airdrop. Try adding some ETH to your account.")
if (airdropFeesResponse.errorCode === Constants.ComputeFeeErrorCode.Infura)
return qsTr("Infura error")
return qsTr("Unknown error")
}
readonly property string totalFee: {
if (!airdropFeesResponse || !Object.values(airdropFeesResponse.totalEthFee).length || !Object.values(airdropFeesResponse.totalFiatFee).length) return ""
if (airdropFeesResponse.errorCode !== Constants.ComputeFeeErrorCode.Success && airdropFeesResponse.errorCode !== Constants.ComputeFeeErrorCode.Balance)
return ""
return `${LocaleUtils.currencyAmountToLocaleString(airdropFeesResponse.totalEthFee)} (${LocaleUtils.currencyAmountToLocaleString(airdropFeesResponse.totalFiatFee)})`
}
readonly property var feesPerContract: {
if (!airdropFeesResponse || !Object.values(airdropFeesResponse.fees).length || totalFee == "") return []
return airdropFeesResponse.fees.map(fee => {
return {
contractUniqueKey: fee.contractUniqueKey,
feeText: `${LocaleUtils.currencyAmountToLocaleString(fee.ethFee)} (${LocaleUtils.currencyAmountToLocaleString(fee.fiatFee)})`
}
})
}
}

View File

@ -5,6 +5,7 @@ import StatusQ.Controls 0.1
import AppLayouts.Communities.layouts 1.0 import AppLayouts.Communities.layouts 1.0
import AppLayouts.Communities.views 1.0 import AppLayouts.Communities.views 1.0
import AppLayouts.Communities.helpers 1.0
import utils 1.0 import utils 1.0
@ -19,6 +20,7 @@ StackView {
required property bool isTokenMasterOwner required property bool isTokenMasterOwner
required property bool isAdmin required property bool isAdmin
readonly property bool isPrivilegedTokenOwnerProfile: root.isOwner || root.isTokenMasterOwner readonly property bool isPrivilegedTokenOwnerProfile: root.isOwner || root.isTokenMasterOwner
readonly property alias airdropFeesSubscriber: d.aidropFeeSubscriber
// Owner and TMaster token related properties: // Owner and TMaster token related properties:
readonly property bool arePrivilegedTokensDeployed: root.isOwnerTokenDeployed && root.isTMasterTokenDeployed readonly property bool arePrivilegedTokensDeployed: root.isOwnerTokenDeployed && root.isTMasterTokenDeployed
@ -32,11 +34,6 @@ StackView {
required property var membersModel required property var membersModel
required property var accountsModel required property var accountsModel
// JS object specifing fees for the airdrop operation, should be set to
// provide response to airdropFeesRequested signal.
// Refer EditAirdropView::airdropFees for details.
property var airdropFees: null
property int viewWidth: 560 // by design property int viewWidth: 560 // by design
property string previousPageName: depth > 1 ? qsTr("Airdrops") : "" property string previousPageName: depth > 1 ? qsTr("Airdrops") : ""
@ -64,7 +61,7 @@ StackView {
id: d id: d
readonly property bool isAdminOnly: root.isAdmin && !root.isPrivilegedTokenOwnerProfile readonly property bool isAdminOnly: root.isAdmin && !root.isPrivilegedTokenOwnerProfile
property AirdropFeesSubscriber aidropFeeSubscriber: null
signal selectToken(string key, string amount, int type) signal selectToken(string key, string amount, int type)
signal addAddresses(var addresses) signal addAddresses(var addresses)
} }
@ -80,7 +77,6 @@ StackView {
text: qsTr("New Airdrop") text: qsTr("New Airdrop")
enabled: !d.isAdminOnly && root.arePrivilegedTokensDeployed enabled: !d.isAdminOnly && root.arePrivilegedTokensDeployed
onClicked: root.push(newAirdropView, StackView.Immediate) onClicked: root.push(newAirdropView, StackView.Immediate)
} }
] ]
@ -121,10 +117,10 @@ StackView {
collectiblesModel: root.collectiblesModel collectiblesModel: root.collectiblesModel
membersModel: root.membersModel membersModel: root.membersModel
accountsModel: root.accountsModel accountsModel: root.accountsModel
totalFeeText: feesSubscriber.totalFee
Binding on airdropFees { feeErrorText: feesSubscriber.feesError
value: root.airdropFees feesPerSelectedContract: feesSubscriber.feesPerContract
} feesAvailable: !!feesSubscriber.airdropFeesResponse
onAirdropClicked: { onAirdropClicked: {
root.airdropClicked(airdropTokens, addresses, feeAccountAddress) root.airdropClicked(airdropTokens, addresses, feeAccountAddress)
@ -136,7 +132,16 @@ StackView {
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) d.aidropFeeSubscriber = feesSubscriber
}
AirdropFeesSubscriber {
id: feesSubscriber
enabled: view.visible && view.showingFees
communityId: view.communityDetails.id
contractKeysAndAmounts: view.selectedContractKeysAndAmounts
addressesToAirdrop: view.selectedAddressesToAirdrop
feeAccountAddress: view.selectedFeeAccount
} }
} }
} }

View File

@ -35,26 +35,40 @@ StatusScrollView {
// A model containing accounts from which the fee can be paid: // A model containing accounts from which the fee can be paid:
required property var accountsModel required property var accountsModel
// JS object specifing fees for the airdrop operation, should be set to // Text to display as total fee
// provide response to airdropFeesRequested signal. required property string totalFeeText
// // Text to display in case of error
// The expected structure is as follows: required property string feeErrorText
// { // Array containing the fees for each token
// fees: [{ // [{contractUniqueKey: string, feeText: string}]
// ethFee: {CurrencyAmount JSON}, required property var feesPerSelectedContract
// fiatFee: {CurrencyAmount JSON}, // Bool property indicating whether the fees are available
// contractUniqueKey: string, required property bool feesAvailable
// 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 {}
// Array containing the contract keys and amounts of the tokens to be airdropped
readonly property alias selectedContractKeysAndAmounts: d.selectedContractKeysAndAmounts
// Array containing the addresses to which the tokens will be airdropped
readonly property alias selectedAddressesToAirdrop: d.selectedAddressesToAirdrop
// The address of the account from which the fee will be paid
readonly property alias selectedFeeAccount: d.selectedFeeAccount
// Bool property indicating whether the fees are shown
readonly property bool showingFees: d.showFees
onFeesPerSelectedContractChanged: {
feesModel.clear()
feesPerSelectedContract.forEach(entry => {
feesModel.append({
contractUniqueKey: entry.contractUniqueKey,
title: qsTr("Airdrop %1 on %2")
.arg(ModelUtils.getByKey(root.selectedHoldingsModel, "contractUniqueKey", entry.contractUniqueKey, "symbol"))
.arg(ModelUtils.getByKey(root.selectedHoldingsModel, "contractUniqueKey", entry.contractUniqueKey, "networkText")),
feeText: entry.feeText
})
})
}
ModelChangeTracker { ModelChangeTracker {
id: holdingsModelTracker id: holdingsModelTracker
@ -80,20 +94,12 @@ StatusScrollView {
signal airdropClicked(var airdropTokens, var addresses, string feeAccountAddress) signal airdropClicked(var airdropTokens, var addresses, string feeAccountAddress)
signal airdropFeesRequested(var contractKeysAndAmounts, var addresses, string feeAccountAddress)
signal navigateToMintTokenSettings(bool isAssetType) signal navigateToMintTokenSettings(bool isAssetType)
function selectToken(key, amount, type) { function selectToken(key, amount, type) {
if(selectedHoldingsModel) if(selectedHoldingsModel)
selectedHoldingsModel.clear() selectedHoldingsModel.clear()
const tokenModel = type === Constants.TokenType.ERC20 ?
root.assetsModel : root.collectiblesModel
const modelItem = PermissionsHelpers.getTokenByKey(
tokenModel, key)
const entry = d.prepareEntry(key, amount, type) const entry = d.prepareEntry(key, amount, type)
entry.valid = true entry.valid = true
selectedHoldingsModel.append(entry) selectedHoldingsModel.append(entry)
@ -103,51 +109,6 @@ StatusScrollView {
addresses.addAddresses(_addresses) addresses.addAddresses(_addresses)
} }
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)})`
}
d.feesError = ""
d.totalFee = ""
if (errorCode === Constants.ComputeFeeErrorCode.Infura) {
d.feesError = qsTr("Infura error")
return
}
if (errorCode === Constants.ComputeFeeErrorCode.Success ||
errorCode === Constants.ComputeFeeErrorCode.Balance) {
fees.forEach(fee => {
const idx = ModelUtils.indexOf(
feesModel, "contractUniqueKey",
fee.contractUniqueKey)
feesModel.set(idx, {
feeText: buildFeeString(fee.ethFee, fee.fiatFee)
})
})
d.totalFee = buildFeeString(
root.airdropFees.totalEthFee,
root.airdropFees.totalFiatFee)
if (errorCode === Constants.ComputeFeeErrorCode.Balance) {
d.feesError = qsTr("Your account does not have enough ETH to pay the gas fee for this airdrop. Try adding some ETH to your account.")
}
return
}
d.feesError = qsTr("Unknown error")
}
QtObject { QtObject {
id: d id: d
@ -155,25 +116,32 @@ StatusScrollView {
readonly property int dropdownHorizontalOffset: 4 readonly property int dropdownHorizontalOffset: 4
readonly property int dropdownVerticalOffset: 1 readonly property int dropdownVerticalOffset: 1
property string feesError
property string totalFee
readonly property bool isFeeLoading:
root.airdropFees === null ||
(root.airdropFees.errorCode !== Constants.ComputeFeeErrorCode.Success &&
root.airdropFees.errorCode !== Constants.ComputeFeeErrorCode.Balance)
readonly property bool showFees: root.selectedHoldingsModel.count > 0 readonly property bool showFees: root.selectedHoldingsModel.count > 0
&& airdropRecipientsSelector.valid && airdropRecipientsSelector.valid
&& airdropRecipientsSelector.count > 0 && airdropRecipientsSelector.count > 0
readonly property int totalRevision: holdingsModelTracker.revision readonly property var selectedContractKeysAndAmounts: {
+ addressesModelTracker.revision //Depedencies:
+ membersModelTracker.revision root.selectedHoldingsModel
+ feesBox.accountsSelector.currentIndex holdingsModelTracker.revision
+ (d.showFees ? 1 : 0)
onTotalRevisionChanged: Qt.callLater(() => d.resetFees()) return ModelUtils.modelToArray(
root.selectedHoldingsModel,
["contractUniqueKey", "amount"])
}
readonly property var selectedAddressesToAirdrop: {
//Dependecies:
addresses
addressesModelTracker.revision
return ModelUtils.modelToArray(
addresses, ["address"]).map(e => e.address)
.concat([...selectedKeysFilter.keys])
}
readonly property string selectedFeeAccount: ModelUtils.get(root.accountsModel,
feesBox.accountIndex).address
function prepareEntry(key, amount, type) { function prepareEntry(key, amount, type) {
const tokenModel = type === Constants.TokenType.ERC20 const tokenModel = type === Constants.TokenType.ERC20
@ -199,61 +167,6 @@ StatusScrollView {
symbol: modelItem.symbol symbol: modelItem.symbol
} }
} }
function rebuildFeesModel() {
feesModel.clear()
const airdropTokens = ModelUtils.modelToArray(
root.selectedHoldingsModel,
["contractUniqueKey", "accountName",
"symbol", "amount", "tokenText",
"networkText"])
airdropTokens.forEach(entry => {
feesModel.append({
contractUniqueKey: entry.contractUniqueKey,
title: qsTr("Airdrop %1 on %2")
.arg(entry.symbol)
.arg(entry.networkText),
feeText: ""
})
})
}
function requestFees() {
const airdropTokens = ModelUtils.modelToArray(
root.selectedHoldingsModel,
["contractUniqueKey", "amount"])
const contractKeysAndAmounts = airdropTokens.map(item => ({
amount: item.amount,
contractUniqueKey: item.contractUniqueKey
}))
const addressesArray = ModelUtils.modelToArray(
addresses, ["address"]).map(e => e.address)
const airdropAddresses = [...selectedKeysFilter.keys]
const accountItem = ModelUtils.get(root.accountsModel,
feesBox.accountIndex)
airdropFeesRequested(contractKeysAndAmounts, addressesArray.concat(airdropAddresses),
accountItem.address)
}
function resetFees() {
root.airdropFees = null
d.feesError = ""
d.totalFee = ""
if (!d.showFees) {
feesModel.clear()
return
}
d.rebuildFeesModel()
d.requestFees()
}
} }
ListModel { ListModel {
@ -622,10 +535,10 @@ StatusScrollView {
model: feesModel model: feesModel
accountsSelector.model: root.accountsModel accountsSelector.model: root.accountsModel
totalFeeText: d.totalFee totalFeeText: root.totalFeeText
placeholderText: qsTr("Add valid “What” and “To” values to see fees") placeholderText: qsTr("Add valid “What” and “To” values to see fees")
accountErrorText: d.feesError accountErrorText: root.feeErrorText
} }
WarningPanel { WarningPanel {
@ -646,7 +559,7 @@ StatusScrollView {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Style.current.bigPadding Layout.topMargin: Style.current.bigPadding
text: qsTr("Create airdrop") text: qsTr("Create airdrop")
enabled: root.isFullyFilled && !d.isFeeLoading && d.feesError === "" enabled: root.isFullyFilled && root.feesAvailable && root.feeErrorText === ""
onClicked: { onClicked: {
const accountItem = ModelUtils.get(root.accountsModel, const accountItem = ModelUtils.get(root.accountsModel,
@ -666,8 +579,8 @@ StatusScrollView {
model: feesModel model: feesModel
totalFeeText: d.totalFee totalFeeText: root.totalFeeText
errorText: d.feesError errorText: root.feeErrorText
onOpened: { onOpened: {
const title1 = qsTr("Sign transaction - Airdrop %n token(s)", "", const title1 = qsTr("Sign transaction - Airdrop %n token(s)", "",
@ -676,8 +589,6 @@ StatusScrollView {
addresses.count + airdropRecipientsSelector.membersModel.count) addresses.count + airdropRecipientsSelector.membersModel.count)
title = `${title1} ${title2}` title = `${title1} ${title2}`
d.resetFees()
} }
onSignTransactionClicked: { onSignTransactionClicked: {