From 1c50ec17a8288fa204f0216194bd6d0dcf470bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cie=C5=9Blak?= Date: Thu, 10 Aug 2023 14:23:59 +0200 Subject: [PATCH] chore(Communities): Refactor amounts handling for displaying, minting, airdropping and burning Closes: #11491 --- .../main/communities/tokens/io_interface.nim | 8 +-- .../communities/tokens/models/token_model.nim | 13 ++-- .../main/communities/tokens/module.nim | 32 ++++------ .../modules/main/communities/tokens/view.nim | 8 +-- src/app_service/common/conversion.nim | 1 + .../community_tokens/dto/community_token.nim | 11 ---- storybook/pages/AirdropsSettingsPanelPage.qml | 13 +++- storybook/pages/HoldingsDropdownPage.qml | 13 +++- .../pages/InlineNetworksComboBoxPage.qml | 20 ++++++- storybook/pages/TokenPanelPage.qml | 27 ++++++++- .../StatusQ/Core/Utils/AmountsArithmetic.qml | 24 +++++++- .../controls/InlineNetworksComboBox.qml | 29 +++++++-- .../controls/ListDropdownContent.qml | 29 +++++---- .../Communities/controls/TokenPanel.qml | 26 +++++--- .../helpers/PermissionsHelpers.qml | 3 + .../Communities/helpers/TokenObject.qml | 5 +- .../panels/AirdropsSettingsPanel.qml | 2 +- .../panels/MintTokensSettingsPanel.qml | 27 ++++++--- .../Communities/panels/TokenInfoPanel.qml | 12 +++- .../Communities/popups/BurnTokensPopup.qml | 59 ++++++++++++++---- .../Communities/popups/HoldingsDropdown.qml | 55 +++++++++++------ .../views/CommunitySettingsView.qml | 4 +- .../Communities/views/EditAirdropView.qml | 54 +++++++++++------ .../views/EditCommunityTokenView.qml | 19 +++--- .../Communities/views/EditOwnerTokenView.qml | 2 +- .../Communities/views/EditPermissionView.qml | 10 ++-- .../Communities/views/MintedTokensView.qml | 2 +- ui/imports/shared/controls/AmountInput.qml | 60 +++++++++++++++---- .../shared/stores/CommunityTokensStore.qml | 12 ++-- 29 files changed, 404 insertions(+), 176 deletions(-) diff --git a/src/app/modules/main/communities/tokens/io_interface.nim b/src/app/modules/main/communities/tokens/io_interface.nim index 0e6f4d8e3b..6b967a1958 100644 --- a/src/app/modules/main/communities/tokens/io_interface.nim +++ b/src/app/modules/main/communities/tokens/io_interface.nim @@ -21,14 +21,14 @@ method computeAirdropFee*(self: AccessInterface, communityId: string, tokensJson method selfDestructCollectibles*(self: AccessInterface, communityId: string, collectiblesToBurnJsonString: string, contractUniqueKey: string) {.base.} = raise newException(ValueError, "No implementation available") -method burnTokens*(self: AccessInterface, communityId: string, contractUniqueKey: string, amount: float64) {.base.} = +method burnTokens*(self: AccessInterface, communityId: string, contractUniqueKey: string, amount: string) {.base.} = raise newException(ValueError, "No implementation available") -method deployCollectibles*(self: AccessInterface, communityId: string, address: string, name: string, symbol: string, description: string, supply: float64, infiniteSupply: bool, transferable: bool, +method deployCollectibles*(self: AccessInterface, communityId: string, address: string, name: string, symbol: string, description: string, supply: string, infiniteSupply: bool, transferable: bool, selfDestruct: bool, chainId: int, imageCropInfoJson: string) {.base.} = raise newException(ValueError, "No implementation available") -method deployAssets*(self: AccessInterface, communityId: string, address: string, name: string, symbol: string, description: string, supply: float64, infiniteSupply: bool, decimals: int, +method deployAssets*(self: AccessInterface, communityId: string, address: string, name: string, symbol: string, description: string, supply: string, infiniteSupply: bool, decimals: int, chainId: int, imageCropInfoJson: string) {.base.} = raise newException(ValueError, "No implementation available") @@ -48,7 +48,7 @@ method computeDeployFee*(self: AccessInterface, chainId: int, accountAddress: st method computeSelfDestructFee*(self: AccessInterface, collectiblesToBurnJsonString: string, contractUniqueKey: string) {.base.} = raise newException(ValueError, "No implementation available") -method computeBurnFee*(self: AccessInterface, contractUniqueKey: string, amount: float64) {.base.} = +method computeBurnFee*(self: AccessInterface, contractUniqueKey: string, amount: string) {.base.} = raise newException(ValueError, "No implementation available") method onDeployFeeComputed*(self: AccessInterface, ethCurrency: CurrencyAmount, fiatCurrency: CurrencyAmount, errorCode: ComputeFeeErrorCode) {.base.} = diff --git a/src/app/modules/main/communities/tokens/models/token_model.nim b/src/app/modules/main/communities/tokens/models/token_model.nim index 662731a63e..95361526a8 100644 --- a/src/app/modules/main/communities/tokens/models/token_model.nim +++ b/src/app/modules/main/communities/tokens/models/token_model.nim @@ -2,6 +2,7 @@ import NimQml, Tables, strformat, sequtils, stint import token_item import token_owners_item import token_owners_model +import ../../../../../../app_service/service/community/dto/community import ../../../../../../app_service/service/community_tokens/dto/community_token import ../../../../../../app_service/service/community_tokens/community_collectible_owner import ../../../../../../app_service/common/utils @@ -32,6 +33,7 @@ type BurnState RemotelyDestructState PrivilegesLevel + MultiplierIndex QtObject: type TokenModel* = ref object of QAbstractListModel @@ -181,7 +183,8 @@ QtObject: ModelRole.Decimals.int:"decimals", ModelRole.BurnState.int:"burnState", ModelRole.RemotelyDestructState.int:"remotelyDestructState", - ModelRole.PrivilegesLevel.int:"privilegesLevel" + ModelRole.PrivilegesLevel.int:"privilegesLevel", + ModelRole.MultiplierIndex.int:"multiplierIndex" }.toTable method data(self: TokenModel, index: QModelIndex, role: int): QVariant = @@ -206,7 +209,7 @@ QtObject: result = newQVariant(item.tokenDto.description) of ModelRole.Supply: # we need to present maxSupply - destructedAmount - result = newQVariant(supplyByType(item.tokenDto.supply - item.destructedAmount, item.tokenDto.tokenType)) + result = newQVariant((item.tokenDto.supply - item.destructedAmount).toString(10)) of ModelRole.InfiniteSupply: result = newQVariant(item.tokenDto.infiniteSupply) of ModelRole.Transferable: @@ -230,7 +233,7 @@ QtObject: of ModelRole.AccountAddress: result = newQVariant(item.tokenDto.deployer) of ModelRole.RemainingSupply: - result = newQVariant(supplyByType(item.remainingSupply, item.tokenDto.tokenType)) + result = newQVariant(item.remainingSupply.toString(10)) of ModelRole.Decimals: result = newQVariant(item.tokenDto.decimals) of ModelRole.BurnState: @@ -240,9 +243,11 @@ QtObject: result = newQVariant(destructStatus) of ModelRole.PrivilegesLevel: result = newQVariant(item.tokenDto.privilegesLevel.int) + of ModelRole.MultiplierIndex: + result = newQVariant(if item.tokenDto.tokenType == TokenType.ERC20: 18 else: 0) proc `$`*(self: TokenModel): string = for i in 0 ..< self.items.len: result &= fmt"""TokenModel: [{i}]:({$self.items[i]}) - """ \ No newline at end of file + """ diff --git a/src/app/modules/main/communities/tokens/module.nim b/src/app/modules/main/communities/tokens/module.nim index 4f991d5762..ca63b4a3b6 100644 --- a/src/app/modules/main/communities/tokens/module.nim +++ b/src/app/modules/main/communities/tokens/module.nim @@ -93,29 +93,17 @@ proc authenticate(self: Module) = else: self.controller.authenticateUser() -# for collectibles conversion is: "1" -> Uint256(1) -# for assets amount is converted to basic units (wei-like): "1.5" -> Uint256(1500000000000000000) -proc convertAmountByTokenType(self: Module, tokenType: TokenType, amount: float64): Uint256 = - const decimals = 18 - case tokenType - of TokenType.ERC721: - return stint.parse($amount.int, Uint256) - of TokenType.ERC20: - return conversion.eth2Wei(amount, decimals) - else: - error "Converting amount - unknown token type", tokenType=tokenType - proc getTokenAndAmountList(self: Module, communityId: string, tokensJsonString: string): seq[CommunityTokenAndAmount] = try: let tokensJson = tokensJsonString.parseJson for token in tokensJson: let contractUniqueKey = token["contractUniqueKey"].getStr let tokenDto = self.controller.findContractByUniqueId(contractUniqueKey) - let amountStr = token["amount"].getFloat + let amountStr = token["amount"].getStr if tokenDto.tokenType == TokenType.Unknown: error "Can't find token for community", contractUniqueKey=contractUniqueKey return @[] - result.add(CommunityTokenAndAmount(communityToken: tokenDto, amount: self.convertAmountByTokenType(tokenDto.tokenType, amountStr))) + result.add(CommunityTokenAndAmount(communityToken: tokenDto, amount: amountStr.parse(Uint256))) except Exception as e: error "Error getTokenAndAmountList", msg = e.msg @@ -148,16 +136,16 @@ method selfDestructCollectibles*(self: Module, communityId: string, collectibles self.tempContractAction = ContractAction.SelfDestruct self.authenticate() -method burnTokens*(self: Module, communityId: string, contractUniqueKey: string, amount: float64) = +method burnTokens*(self: Module, communityId: string, contractUniqueKey: string, amount: string) = let tokenDto = self.controller.findContractByUniqueId(contractUniqueKey) self.tempCommunityId = communityId self.tempContractUniqueKey = contractUniqueKey - self.tempAmount = self.convertAmountByTokenType(tokenDto.tokenType, amount) + self.tempAmount = amount.parse(Uint256) self.tempContractAction = ContractAction.Burn self.authenticate() method deployCollectibles*(self: Module, communityId: string, fromAddress: string, name: string, symbol: string, description: string, - supply: float64, infiniteSupply: bool, transferable: bool, selfDestruct: bool, chainId: int, imageCropInfoJson: string) = + supply: string, infiniteSupply: bool, transferable: bool, selfDestruct: bool, chainId: int, imageCropInfoJson: string) = let ownerToken = self.controller.getOwnerToken(communityId) let masterToken = self.controller.getTokenMasterToken(communityId) @@ -170,7 +158,7 @@ method deployCollectibles*(self: Module, communityId: string, fromAddress: strin self.tempChainId = chainId self.tempDeploymentParams.name = name self.tempDeploymentParams.symbol = symbol - self.tempDeploymentParams.supply = self.convertAmountByTokenType(TokenType.ERC721, supply) + self.tempDeploymentParams.supply = supply.parse(Uint256) self.tempDeploymentParams.infiniteSupply = infiniteSupply self.tempDeploymentParams.transferable = transferable self.tempDeploymentParams.remoteSelfDestruct = selfDestruct @@ -207,14 +195,14 @@ method deployOwnerToken*(self: Module, communityId: string, fromAddress: string, self.tempContractAction = ContractAction.DeployOwnerToken self.authenticate() -method deployAssets*(self: Module, communityId: string, fromAddress: string, name: string, symbol: string, description: string, supply: float64, infiniteSupply: bool, decimals: int, +method deployAssets*(self: Module, communityId: string, fromAddress: string, name: string, symbol: string, description: string, supply: string, infiniteSupply: bool, decimals: int, chainId: int, imageCropInfoJson: string) = self.tempAddressFrom = fromAddress self.tempCommunityId = communityId self.tempChainId = chainId self.tempDeploymentParams.name = name self.tempDeploymentParams.symbol = symbol - self.tempDeploymentParams.supply = self.convertAmountByTokenType(TokenType.ERC20, supply) + self.tempDeploymentParams.supply = supply.parse(Uint256) self.tempDeploymentParams.infiniteSupply = infiniteSupply self.tempDeploymentParams.decimals = decimals self.tempDeploymentParams.tokenUri = utl.changeCommunityKeyCompression(communityId) & "/" @@ -269,9 +257,9 @@ method computeSelfDestructFee*(self: Module, collectiblesToBurnJsonString: strin let walletAndAmountList = self.getWalletAndAmountListFromJson(collectiblesToBurnJsonString) self.controller.computeSelfDestructFee(walletAndAmountList, contractUniqueKey) -method computeBurnFee*(self: Module, contractUniqueKey: string, amount: float64) = +method computeBurnFee*(self: Module, contractUniqueKey: string, amount: string) = let tokenDto = self.controller.findContractByUniqueId(contractUniqueKey) - self.controller.computeBurnFee(contractUniqueKey, self.convertAmountByTokenType(tokenDto.tokenType, amount)) + self.controller.computeBurnFee(contractUniqueKey, amount.parse(Uint256)) proc createUrl(self: Module, chainId: int, transactionHash: string): string = let network = self.controller.getNetwork(chainId) diff --git a/src/app/modules/main/communities/tokens/view.nim b/src/app/modules/main/communities/tokens/view.nim index be0a4bc859..ea6da1bb0b 100644 --- a/src/app/modules/main/communities/tokens/view.nim +++ b/src/app/modules/main/communities/tokens/view.nim @@ -21,10 +21,10 @@ QtObject: result.QObject.setup result.communityTokensModule = communityTokensModule - proc deployCollectible*(self: View, communityId: string, fromAddress: string, name: string, symbol: string, description: string, supply: float, infiniteSupply: bool, transferable: bool, selfDestruct: bool, chainId: int, imageCropInfoJson: string) {.slot.} = + proc deployCollectible*(self: View, communityId: string, fromAddress: string, name: string, symbol: string, description: string, supply: string, infiniteSupply: bool, transferable: bool, selfDestruct: bool, chainId: int, imageCropInfoJson: string) {.slot.} = self.communityTokensModule.deployCollectibles(communityId, fromAddress, name, symbol, description, supply, infiniteSupply, transferable, selfDestruct, chainId, imageCropInfoJson) - proc deployAssets*(self: View, communityId: string, fromAddress: string, name: string, symbol: string, description: string, supply: float, infiniteSupply: bool, decimals: int, chainId: int, imageCropInfoJson: string) {.slot.} = + proc deployAssets*(self: View, communityId: string, fromAddress: string, name: string, symbol: string, description: string, supply: string, infiniteSupply: bool, decimals: int, chainId: int, imageCropInfoJson: string) {.slot.} = self.communityTokensModule.deployAssets(communityId, fromAddress, name, symbol, description, supply, infiniteSupply, decimals, chainId, imageCropInfoJson) proc deployOwnerToken*(self:View, communityId: string, fromAddress: string, ownerName: string, ownerSymbol: string, ownerDescription: string, masterName: string, masterSymbol: string, masterDescription: string, chainId: int, imageCropInfoJson: string) {.slot.} = @@ -42,7 +42,7 @@ QtObject: proc selfDestructCollectibles*(self: View, communityId: string, collectiblesToBurnJsonString: string, contractUniqueKey: string) {.slot.} = self.communityTokensModule.selfDestructCollectibles(communityId, collectiblesToBurnJsonString, contractUniqueKey) - proc burnTokens*(self: View, communityId: string, contractUniqueKey: string, amount: float) {.slot.} = + proc burnTokens*(self: View, communityId: string, contractUniqueKey: string, amount: string) {.slot.} = self.communityTokensModule.burnTokens(communityId, contractUniqueKey, amount) proc deployFeeUpdated*(self: View, ethCurrency: QVariant, fiatCurrency: QVariant, errorCode: int) {.signal.} @@ -56,7 +56,7 @@ QtObject: proc computeSelfDestructFee*(self: View, collectiblesToBurnJsonString: string, contractUniqueKey: string) {.slot.} = self.communityTokensModule.computeSelfDestructFee(collectiblesToBurnJsonString, contractUniqueKey) - proc computeBurnFee*(self: View, contractUniqueKey: string, amount: float) {.slot.} = + proc computeBurnFee*(self: View, contractUniqueKey: string, amount: string) {.slot.} = self.communityTokensModule.computeBurnFee(contractUniqueKey, amount) proc updateDeployFee*(self: View, ethCurrency: CurrencyAmount, fiatCurrency: CurrencyAmount, errorCode: int) = diff --git a/src/app_service/common/conversion.nim b/src/app_service/common/conversion.nim index 5c57f643d5..be21eaa562 100644 --- a/src/app_service/common/conversion.nim +++ b/src/app_service/common/conversion.nim @@ -45,6 +45,7 @@ proc toUInt256*(flt: float): UInt256 = proc toUInt64*(flt: float): StUInt[64] = toStUInt(flt, StUInt[64]) +# This method may introduce distortions and should be avoided if possible. proc eth2Wei*(eth: float, decimals: int = 18): UInt256 = let weiValue = eth * parseFloat(alignLeft("1", decimals + 1, '0')) weiValue.toUInt256 diff --git a/src/app_service/service/community_tokens/dto/community_token.nim b/src/app_service/service/community_tokens/dto/community_token.nim index 2b3dec4630..6ef91a9d68 100644 --- a/src/app_service/service/community_tokens/dto/community_token.nim +++ b/src/app_service/service/community_tokens/dto/community_token.nim @@ -89,14 +89,3 @@ proc toCommunityTokenDto*(jsonObj: JsonNode): CommunityTokenDto = proc parseCommunityTokens*(response: RpcResponse[JsonNode]): seq[CommunityTokenDto] = result = map(response.result.getElems(), proc(x: JsonNode): CommunityTokenDto = x.toCommunityTokenDto()) - -proc supplyByType*(supply: Uint256, tokenType: TokenType): float64 = - try: - var eths: string - if tokenType == TokenType.ERC20: - eths = wei2Eth(supply, 18) - else: - eths = supply.toString(10) - return parseFloat(eths) - except Exception as e: - error "Error parsing supply by type ", msg=e.msg, supply=supply, tokenType=tokenType \ No newline at end of file diff --git a/storybook/pages/AirdropsSettingsPanelPage.qml b/storybook/pages/AirdropsSettingsPanelPage.qml index 2dec38257c..7d98eb8723 100644 --- a/storybook/pages/AirdropsSettingsPanelPage.qml +++ b/storybook/pages/AirdropsSettingsPanelPage.qml @@ -104,6 +104,8 @@ SplitView { assetsModel: AssetsModel {} collectiblesModel: ListModel {} + accountsModel: ListModel {} + CollectiblesModel { id: collectiblesModel } @@ -118,6 +120,10 @@ SplitView { name: "supply" expression: ((model.index + 1) * 115).toString() }, + ExpressionRole { + name: "multiplierIndex" + expression: 0 + }, ExpressionRole { name: "infiniteSupply" expression: !(model.index % 4) @@ -158,7 +164,12 @@ SplitView { proxyRoles: [ ExpressionRole { name: "supply" - expression: ((model.index + 1) * 258).toString() + expression: ((model.index + 1) * 584).toString() + + "0".repeat(18) + }, + ExpressionRole { + name: "multiplierIndex" + expression: 18 }, ExpressionRole { name: "infiniteSupply" diff --git a/storybook/pages/HoldingsDropdownPage.qml b/storybook/pages/HoldingsDropdownPage.qml index ede86d3808..fb8658cc6d 100644 --- a/storybook/pages/HoldingsDropdownPage.qml +++ b/storybook/pages/HoldingsDropdownPage.qml @@ -66,7 +66,11 @@ SplitView { proxyRoles: [ ExpressionRole { name: "supply" - expression: model.index === 1 ? model.index : (model.index + 1) * 115 + expression: (model.index === 1 ? 1 : (model.index + 1) * 115).toString() + }, + ExpressionRole { + name: "multiplierIndex" + expression: 0 }, ExpressionRole { name: "infiniteSupply" @@ -99,7 +103,12 @@ SplitView { proxyRoles: [ ExpressionRole { name: "supply" - expression: (model.index + 1) * 584 + expression: ((model.index + 1) * 584).toString() + + "0".repeat(18) + }, + ExpressionRole { + name: "multiplierIndex" + expression: 18 }, ExpressionRole { name: "infiniteSupply" diff --git a/storybook/pages/InlineNetworksComboBoxPage.qml b/storybook/pages/InlineNetworksComboBoxPage.qml index 8731a0e7b3..1c31fdef41 100644 --- a/storybook/pages/InlineNetworksComboBoxPage.qml +++ b/storybook/pages/InlineNetworksComboBoxPage.qml @@ -13,20 +13,30 @@ Item { { name: "Optimism", icon: Style.svg(ModelsData.networks.optimism), - amount: 300, + amount: "300", + multiplierIndex: 0, infiniteAmount: false }, { name: "Arbitrum", icon: Style.svg(ModelsData.networks.arbitrum), - amount: 400, + amount: "400000", + multiplierIndex: 3, infiniteAmount: false }, { name: "Hermez", icon: Style.svg(ModelsData.networks.hermez), - amount: 0, + amount: "0", + multiplierIndex: 0, infiniteAmount: true + }, + { + name: "Ethereum", + icon: Style.svg(ModelsData.networks.ethereum), + amount: "12" + "0".repeat(18), + multiplierIndex: 18, + infiniteAmount: false } ] @@ -78,6 +88,10 @@ Item { Layout.alignment: Qt.AlignHCenter text: `current amount: ${comboBox.currentAmount}` } + Label { + Layout.alignment: Qt.AlignHCenter + text: `current multiplier index: ${comboBox.currentMultiplierIndex}` + } Label { Layout.alignment: Qt.AlignHCenter text: `current amount infinite: ${comboBox.currentInfiniteAmount}` diff --git a/storybook/pages/TokenPanelPage.qml b/storybook/pages/TokenPanelPage.qml index caf3c34e46..78ca30cfa4 100644 --- a/storybook/pages/TokenPanelPage.qml +++ b/storybook/pages/TokenPanelPage.qml @@ -2,6 +2,8 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import Qt.labs.settings 1.0 + import Models 1.0 import Storybook 1.0 import utils 1.0 @@ -20,20 +22,30 @@ SplitView { { name: "Optimism", icon: Style.svg(ModelsData.networks.optimism), - amount: 300, + amount: "300", + multiplierIndex: 0, infiniteAmount: false }, { name: "Arbitrum", icon: Style.svg(ModelsData.networks.arbitrum), - amount: 400, + amount: "400000", + multiplierIndex: 3, infiniteAmount: false }, { name: "Hermez", icon: Style.svg(ModelsData.networks.hermez), - amount: 500, + amount: "0", + multiplierIndex: 0, infiniteAmount: true + }, + { + name: "Ethereum", + icon: Style.svg(ModelsData.networks.ethereum), + amount: "12" + "0".repeat(18), + multiplierIndex: 18, + infiniteAmount: false } ] @@ -119,8 +131,17 @@ SplitView { text: "∞" } } + + Label { + text: "amount: " + tokenPanel.amount + } } } + + Settings { + property alias networksModelCheckBoxChecked: + networksModelCheckBox.checked + } } // category: Panels diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/AmountsArithmetic.qml b/ui/StatusQ/src/StatusQ/Core/Utils/AmountsArithmetic.qml index 4ed5087519..e35a75c666 100644 --- a/ui/StatusQ/src/StatusQ/Core/Utils/AmountsArithmetic.qml +++ b/ui/StatusQ/src/StatusQ/Core/Utils/AmountsArithmetic.qml @@ -40,13 +40,14 @@ QtObject { console.assert(!isNaN(number) && Number.isInteger(multiplier) && multiplier >= 0) const amount = new Big.Big(number).times(10 ** multiplier) - console.assert(amount.eq(amount.round())) + // TODO: restore assert when permissions handled as bigints + // console.assert(amount.eq(amount.round())) return amount } /*! \qmlmethod AmountsArithmetic::toNumber(amount, multiplier = 0) - \brief Converts an amount to a java script number. + \brief Converts an amount (in form of amount object or string) to a java script number. This operation may result in loss of precision. Because of that it should be used only to display a value in the user interface, but requires @@ -57,10 +58,16 @@ QtObject { \qml console.log(AmountsArithmetic.toNumber( AmountsArithmetic.fromString("123456789123456789123"))) // 123456789123456800000 + console.log(AmountsArithmetic.toNumber("123456789123456789123")) // 123456789123456800000 \endqml */ function toNumber(amount, multiplier = 0) { console.assert(Number.isInteger(multiplier) && multiplier >= 0) + + if (typeof amount === "string") + amount = fromString(amount) + + console.assert(amount instanceof Big.Big) return amount.div(10 ** multiplier).toNumber() } @@ -84,7 +91,8 @@ QtObject { function fromString(numStr) { console.assert(typeof numStr === "string") const amount = new Big.Big(numStr) - console.assert(amount.eq(amount.round())) + // TODO: restore assert when permissions handled as bigints + //console.assert(amount.eq(amount.round())) return amount } @@ -105,6 +113,16 @@ QtObject { return amount.times(multiplier) } + /*! + \qmlmethod AmountsArithmetic::div(divident, divisor) + \brief Returns a Big number whose value is the value of divident divided by divisor. + */ + function div(divident, divisor) { + console.assert(divident instanceof Big.Big) + console.assert(divisor instanceof Big.Big) + return divident.div(divisor) + } + /*! \qmlmethod AmountsArithmetic::cmp(amount1, amount2) \brief Compares two amounts. diff --git a/ui/app/AppLayouts/Communities/controls/InlineNetworksComboBox.qml b/ui/app/AppLayouts/Communities/controls/InlineNetworksComboBox.qml index 75eb377dc7..18c577d6e3 100644 --- a/ui/app/AppLayouts/Communities/controls/InlineNetworksComboBox.qml +++ b/ui/app/AppLayouts/Communities/controls/InlineNetworksComboBox.qml @@ -5,6 +5,7 @@ import QtQml 2.15 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Controls 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils import SortFilterProxyModel 0.2 @@ -13,6 +14,7 @@ StatusComboBox { readonly property string currentName: control.currentText readonly property alias currentAmount: instantiator.amount + readonly property alias currentMultiplierIndex: instantiator.multiplierIndex readonly property alias currentInfiniteAmount: instantiator.infiniteAmount readonly property alias currentIcon: instantiator.icon @@ -49,6 +51,10 @@ StatusComboBox { readonly property int iconSize: 32 readonly property string infinitySymbol: "∞" + + function amountText(amount, multiplierIndex) { + return SQUtils.AmountsArithmetic.toNumber(amount, multiplierIndex) + } } component CustomText: StatusBaseText { @@ -94,7 +100,8 @@ StatusComboBox { id: instantiator property string icon - property int amount + property string amount + property int multiplierIndex property bool infiniteAmount model: SortFilterProxyModel { @@ -109,6 +116,7 @@ StatusComboBox { readonly property list bindings: [ Bind { property: "icon"; value: model.icon }, Bind { property: "amount"; value: model.amount }, + Bind { property: "multiplierIndex"; value: model.multiplierIndex }, Bind { property: "infiniteAmount"; value: model.infiniteAmount } ] } @@ -118,10 +126,17 @@ StatusComboBox { title: root.control.displayText iconSource: instantiator.icon - amount: !d.oneItem - ? (instantiator.infiniteAmount ? d.infinitySymbol - : instantiator.amount) - : "" + amount: { + if (d.oneItem || !instantiator.amount) + return "" + + if (instantiator.infiniteAmount) + return d.infinitySymbol + + return d.amountText(instantiator.amount, + instantiator.multiplierIndex) + } + cursorShape: d.oneItem ? Qt.ArrowCursor : Qt.PointingHandCursor onClicked: { @@ -135,7 +150,9 @@ StatusComboBox { delegate: DelegateItem { title: model.name iconSource: model.icon - amount: model.infiniteAmount ? d.infinitySymbol : model.amount + amount: model.infiniteAmount + ? d.infinitySymbol + : d.amountText(model.amount, model.multiplierIndex) width: root.width height: root.height diff --git a/ui/app/AppLayouts/Communities/controls/ListDropdownContent.qml b/ui/app/AppLayouts/Communities/controls/ListDropdownContent.qml index d3f2ec481b..a7cf468375 100644 --- a/ui/app/AppLayouts/Communities/controls/ListDropdownContent.qml +++ b/ui/app/AppLayouts/Communities/controls/ListDropdownContent.qml @@ -1,12 +1,13 @@ -import QtQuick 2.13 -import QtQuick.Layouts 1.14 -import QtQuick.Controls 2.13 -import QtGraphicalEffects 1.13 +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.15 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Components 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils import utils 1.0 @@ -98,12 +99,20 @@ StatusListView { showSubItemsIcon: !!model.subItems && model.subItems.count > 0 selected: root.checkedKeys.includes(model.key) amount: { - if(!model.infiniteSupply && !!model.supply && model.supply == 1) + if (model.supply === undefined + || model.multiplierIndex === undefined) + return "" + + if (model.infiniteSupply) + return "∞" + + if (model.supply === "1") return qsTr("Max. 1") - if(root.showTokenAmount) - return !!model.infiniteSupply ? "∞" : model.supply ?? "" - + if (root.showTokenAmount) + return SQUtils.AmountsArithmetic.toNumber(model.supply, + model.multiplierIndex) + return "" } diff --git a/ui/app/AppLayouts/Communities/controls/TokenPanel.qml b/ui/app/AppLayouts/Communities/controls/TokenPanel.qml index 189950b911..b39e31efc7 100644 --- a/ui/app/AppLayouts/Communities/controls/TokenPanel.qml +++ b/ui/app/AppLayouts/Communities/controls/TokenPanel.qml @@ -1,5 +1,5 @@ -import QtQuick 2.14 -import QtQuick.Layouts 1.14 +import QtQuick 2.15 +import QtQuick.Layouts 1.15 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 @@ -21,6 +21,7 @@ ColumnLayout { property alias tokenImage: item.iconSource property alias amountText: amountInput.text property alias amount: amountInput.amount + property alias multiplierIndex: amountInput.multiplierIndex property alias tokenCategoryText: tokenLabel.text property alias networkLabelText: d.networkLabelText property alias addOrUpdateButtonEnabled: addOrUpdateButton.enabled @@ -33,8 +34,9 @@ ColumnLayout { signal updateClicked signal removeClicked - function setAmount(amount) { - amountInput.setAmount(amount) + function setAmount(amount, multiplierIndex = 0) { + console.assert(typeof amount === "string") + amountInput.setAmount(amount, multiplierIndex) } QtObject { @@ -86,7 +88,10 @@ ColumnLayout { spacing: 10 property alias currentAmount: inlineNetworksComboBox.currentAmount - property alias currentInfiniteAmount: inlineNetworksComboBox.currentInfiniteAmount + property alias currentMultiplierIndex: + inlineNetworksComboBox.currentMultiplierIndex + property alias currentInfiniteAmount: + inlineNetworksComboBox.currentInfiniteAmount CustomText { id: networkLabel @@ -124,10 +129,14 @@ ColumnLayout { !networksComboBoxLoader.item.currentInfiniteAmount maximumAmount: !!networksComboBoxLoader.item - ? networksComboBoxLoader.item.currentAmount : 0 + ? networksComboBoxLoader.item.currentAmount : "0" + + multiplierIndex: !!networksComboBoxLoader.item + ? networksComboBoxLoader.item.currentMultiplierIndex : 0 onKeyPressed: { - if(!addOrUpdateButton.enabled) return + if(!addOrUpdateButton.enabled) + return if(event.key === Qt.Key_Enter || event.key === Qt.Key_Return) addOrUpdateButton.clicked() @@ -145,7 +154,8 @@ ColumnLayout { StatusButton { id: addOrUpdateButton - text: (root.mode === HoldingTypes.Mode.Add) ? qsTr("Add") : qsTr("Update") + text: root.mode === HoldingTypes.Mode.Add ? qsTr("Add") + : qsTr("Update") Layout.preferredHeight: d.defaultHeight Layout.topMargin: d.defaultSpacing Layout.fillWidth: true diff --git a/ui/app/AppLayouts/Communities/helpers/PermissionsHelpers.qml b/ui/app/AppLayouts/Communities/helpers/PermissionsHelpers.qml index 62fb004c90..fbb8c82dd2 100644 --- a/ui/app/AppLayouts/Communities/helpers/PermissionsHelpers.qml +++ b/ui/app/AppLayouts/Communities/helpers/PermissionsHelpers.qml @@ -73,6 +73,9 @@ QtObject { } function setHoldingsTextFormat(type, name, amount) { + if (typeof amount === "string") + amount = AmountsArithmetic.toNumber(AmountsArithmetic.fromString(amount)) + switch (type) { case HoldingTypes.Type.Asset: return `${LocaleUtils.numberToLocaleString(amount)} ${name}` diff --git a/ui/app/AppLayouts/Communities/helpers/TokenObject.qml b/ui/app/AppLayouts/Communities/helpers/TokenObject.qml index 281a7bcddd..8d016f772f 100644 --- a/ui/app/AppLayouts/Communities/helpers/TokenObject.qml +++ b/ui/app/AppLayouts/Communities/helpers/TokenObject.qml @@ -22,8 +22,9 @@ QtObject { property string symbol property string description property bool infiniteSupply: true - property int supply: 1 - property int remainingTokens: supply + property string supply: "1" + property string remainingTokens: supply + property int multiplierIndex: 0 // Artwork related properties: property url artworkSource diff --git a/ui/app/AppLayouts/Communities/panels/AirdropsSettingsPanel.qml b/ui/app/AppLayouts/Communities/panels/AirdropsSettingsPanel.qml index 49c075334a..c85e162044 100644 --- a/ui/app/AppLayouts/Communities/panels/AirdropsSettingsPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/AirdropsSettingsPanel.qml @@ -65,7 +65,7 @@ StackView { readonly property bool isAdminOnly: root.isAdmin && !root.isPrivilegedTokenOwnerProfile - signal selectToken(string key, int amount, int type) + signal selectToken(string key, string amount, int type) signal addAddresses(var addresses) } diff --git a/ui/app/AppLayouts/Communities/panels/MintTokensSettingsPanel.qml b/ui/app/AppLayouts/Communities/panels/MintTokensSettingsPanel.qml index 94d22b59a2..2390a2a774 100644 --- a/ui/app/AppLayouts/Communities/panels/MintTokensSettingsPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/MintTokensSettingsPanel.qml @@ -66,16 +66,16 @@ StackView { signal mintOwnerToken(var ownerToken, var tMasterToken) signal deployFeesRequested(int chainId, string accountAddress, int tokenType) - signal burnFeesRequested(string tokenKey, int amount, string accountAddress) + signal burnFeesRequested(string tokenKey, string amount, string accountAddress) signal remotelyDestructFeesRequest(var remotelyDestructTokensList, // [key , amount] string tokenKey, string accountAddress) signal remotelyDestructCollectibles(var remotelyDestructTokensList, // [key , amount] string tokenKey, string accountAddress) - signal signBurnTransactionOpened(string tokenKey, int amount, string accountAddress) - signal burnToken(string tokenKey, int amount, string accountAddress) - signal airdropToken(string tokenKey, int type, var addresses) + signal signBurnTransactionOpened(string tokenKey, string amount, string accountAddress) + signal burnToken(string tokenKey, string amount, string accountAddress) + signal airdropToken(string tokenKey, string amount, int type, var addresses) signal deleteToken(string tokenKey) function setFeeLoading() { @@ -281,6 +281,7 @@ StackView { property TokenObject asset: TokenObject{ type: Constants.TokenType.ERC20 + multiplierIndex: 18 } property TokenObject collectible: TokenObject { @@ -520,11 +521,15 @@ StackView { token: TokenObject {} onGeneralAirdropRequested: { - root.airdropToken(view.airdropKey, view.token.type, []) // tokenKey instead when backend airdrop ready to use key instead of symbol + root.airdropToken(view.airdropKey, + "1" + "0".repeat(view.token.multiplierIndex), + view.token.type, []) // tokenKey instead when backend airdrop ready to use key instead of symbol } onAirdropRequested: { - root.airdropToken(view.airdropKey, view.token.type, [address]) // tokenKey instead when backend airdrop ready to use key instead of symbol + root.airdropToken(view.airdropKey, + "1" + "0".repeat(view.token.multiplierIndex), + view.token.type, [address]) // tokenKey instead when backend airdrop ready to use key instead of symbol } onRemoteDestructRequested: { @@ -609,15 +614,17 @@ StackView { remotelyDestructVisible: token.remotelyDestruct burnVisible: !token.infiniteSupply - onAirdropClicked: root.airdropToken(view.airdropKey, // tokenKey instead when backend airdrop ready to use key instead of symbol - view.token.type, []) + onAirdropClicked: root.airdropToken( + view.airdropKey, + "1" + "0".repeat(view.token.multiplierIndex), + view.token.type, []) onRemotelyDestructClicked: remotelyDestructPopup.open() onBurnClicked: burnTokensPopup.open() // helper properties to pass data through popups property var remotelyDestructTokensList - property int burnAmount + property string burnAmount property string accountAddress RemotelyDestructPopup { @@ -703,6 +710,7 @@ StackView { communityName: root.communityName tokenName: footer.token.name remainingTokens: footer.token.remainingTokens + multiplierIndex: footer.token.multiplierIndex tokenSource: footer.token.artworkSource chainName: footer.token.chainName @@ -786,6 +794,7 @@ StackView { token.burnState: model.burnState token.remotelyDestructState: model.remotelyDestructState token.accountAddress: model.accountAddress + token.multiplierIndex: model.multiplierIndex } onCountChanged: { diff --git a/ui/app/AppLayouts/Communities/panels/TokenInfoPanel.qml b/ui/app/AppLayouts/Communities/panels/TokenInfoPanel.qml index 162432efb7..a5b8556ebe 100644 --- a/ui/app/AppLayouts/Communities/panels/TokenInfoPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/TokenInfoPanel.qml @@ -177,7 +177,11 @@ Control { id: totalbox label: qsTr("Total") - value: token.infiniteSupply ? d.infiniteSymbol : LocaleUtils.numberToLocaleString(token.supply) + value: token.infiniteSupply + ? d.infiniteSymbol + : LocaleUtils.numberToLocaleString( + StatusQUtils.AmountsArithmetic.toNumber(token.supply, + token.multiplierIndex)) isLoading: !token.infiniteSupply && ((!root.isAssetPanel && token.remotelyDestructState === Constants.ContractTransactionStatus.InProgress) || (d.burnState === Constants.ContractTransactionStatus.InProgress)) @@ -189,7 +193,11 @@ Control { readonly property int remainingTokens: root.preview ? token.supply : token.remainingTokens label: qsTr("Remaining") - value: token.infiniteSupply ? d.infiniteSymbol : LocaleUtils.numberToLocaleString(remainingTokens) + value: token.infiniteSupply + ? d.infiniteSymbol + : LocaleUtils.numberToLocaleString( + StatusQUtils.AmountsArithmetic.toNumber(token.remainingTokens, + token.multiplierIndex)) isLoading: !token.infiniteSupply && (d.burnState === Constants.ContractTransactionStatus.InProgress) } diff --git a/ui/app/AppLayouts/Communities/popups/BurnTokensPopup.qml b/ui/app/AppLayouts/Communities/popups/BurnTokensPopup.qml index 5016819779..d7ce916c5c 100644 --- a/ui/app/AppLayouts/Communities/popups/BurnTokensPopup.qml +++ b/ui/app/AppLayouts/Communities/popups/BurnTokensPopup.qml @@ -25,7 +25,8 @@ StatusDialog { property string communityName property bool isAsset // If asset isAsset = true; if collectible --> isAsset = false property string tokenName - property int remainingTokens + property string remainingTokens + property int multiplierIndex property url tokenSource property string chainName @@ -38,13 +39,20 @@ StatusDialog { // Account expected roles: address, name, color, emoji, walletType property var accounts - signal burnClicked(int burnAmount, string accountAddress) + signal burnClicked(string burnAmount, string accountAddress) signal cancelClicked - signal burnFeesRequested(int burnAmount, string accountAddress) + signal burnFeesRequested(string burnAmount, string accountAddress) QtObject { id: d + readonly property real remainingTokensFloat: + SQUtils.AmountsArithmetic.toNumber( + root.remainingTokens, root.multiplierIndex) + + readonly property string remainingTokensDisplayText: + LocaleUtils.numberToLocaleString(remainingTokensFloat) + property string accountAddress property alias amountToBurn: amountToBurnInput.text readonly property bool isFeeError: root.feeErrorText !== "" @@ -75,7 +83,7 @@ StatusDialog { StatusBaseText { Layout.fillWidth: true - text: qsTr("How many of %1’s remaining %n %2 tokens would you like to burn?", "", root.remainingTokens).arg(root.communityName).arg(root.tokenName) + text: qsTr("How many of %1’s remaining %n %2 tokens would you like to burn?", "", d.remainingTokensFloat).arg(root.communityName).arg(root.tokenName) wrapMode: Text.WordWrap lineHeight: 1.2 font.pixelSize: Style.current.primaryTextFontSize @@ -107,7 +115,19 @@ StatusDialog { validationMode: StatusInput.ValidationMode.OnlyWhenDirty validators: [ StatusValidator { - validate: (value) => { return (parseInt(value) > 0 && parseInt(value) <= root.remainingTokens) } + validate: (value) => { + const intAmount = parseInt(value) + + if (!intAmount) + return false + + const current = SQUtils.AmountsArithmetic.fromNumber( + intAmount, root.multiplierIndex) + const remaining = SQUtils.AmountsArithmetic.fromString( + root.remainingTokens) + + return SQUtils.AmountsArithmetic.cmp(current, remaining) <= 0 + } errorMessage: qsTr("Exceeds available remaining") }, StatusValidator { @@ -127,7 +147,7 @@ StatusDialog { Layout.alignment: Qt.AlignTop - text: qsTr("All available remaining (%1)").arg(root.remainingTokens) + text: qsTr("All available remaining (%1)").arg(d.remainingTokensDisplayText) font.pixelSize: Style.current.primaryTextFontSize ButtonGroup.group: radioGroup } @@ -173,10 +193,18 @@ StatusDialog { interval: 500 onTriggered: { - if(specificAmountButton.checked) - root.burnFeesRequested(parseInt(amountToBurnInput.text), d.accountAddress) - else + if(specificAmountButton.checked) { + if (!amountToBurnInput.text) + return + + root.burnFeesRequested( + SQUtils.AmountsArithmetic.fromNumber( + parseInt(amountToBurnInput.text), + root.multiplierIndex), + d.accountAddress) + } else { root.burnFeesRequested(root.remainingTokens, d.accountAddress) + } } } @@ -193,7 +221,7 @@ StatusDialog { header: StatusDialogHeader { headline.title: qsTr("Burn %1 tokens").arg(root.tokenName) - headline.subtitle: qsTr("%n %1 remaining in smart contract", "", root.remainingTokens).arg(root.tokenName) + headline.subtitle: qsTr("%n %1 remaining in smart contract", "", d.remainingTokensFloat).arg(root.tokenName) leftComponent: Rectangle { height: 40 width: height @@ -236,10 +264,15 @@ StatusDialog { text: qsTr("Burn tokens") type: StatusBaseButton.Type.Danger onClicked: { - if(specificAmountButton.checked) - root.burnClicked(parseInt(amountToBurnInput.text), d.accountAddress) - else + if(specificAmountButton.checked) { + root.burnClicked( + SQUtils.AmountsArithmetic.fromNumber( + parseInt(amountToBurnInput.text), + root.multiplierIndex), + d.accountAddress) + } else { root.burnClicked(root.remainingTokens, d.accountAddress) + } } } } diff --git a/ui/app/AppLayouts/Communities/popups/HoldingsDropdown.qml b/ui/app/AppLayouts/Communities/popups/HoldingsDropdown.qml index 2a3436c494..81d35d44a6 100644 --- a/ui/app/AppLayouts/Communities/popups/HoldingsDropdown.qml +++ b/ui/app/AppLayouts/Communities/popups/HoldingsDropdown.qml @@ -35,20 +35,21 @@ StatusDropdown { property var usedEnsNames: [] property string assetKey: "" - property real assetAmount: 0 + property string assetAmount: "0" + property int assetMultiplierIndex: 0 property string collectibleKey: "" - property real collectibleAmount: 1 + property string collectibleAmount: "1" property string ensDomainName: "" property bool showTokenAmount: true - signal addAsset(string key, real amount) - signal addCollectible(string key, real amount) + signal addAsset(string key, string amount) + signal addCollectible(string key, string amount) signal addEns(string domain) - signal updateAsset(string key, real amount) - signal updateCollectible(string key, real amount) + signal updateAsset(string key, string amount) + signal updateCollectible(string key, string amount) signal updateEns(string domain) signal removeClicked @@ -94,8 +95,10 @@ StatusDropdown { ] readonly property var tabsModel: [qsTr("Assets"), qsTr("Collectibles"), qsTr("ENS")] readonly property var tabsModelNoEns: [qsTr("Assets"), qsTr("Collectibles")] - readonly property bool assetsReady: root.assetAmount > 0 && root.assetKey - readonly property bool collectiblesReady: root.collectibleAmount > 0 && root.collectibleKey + + readonly property bool assetsReady: root.assetAmount !== "0" && root.assetKey + readonly property bool collectiblesReady: root.collectibleAmount !== "0" && root.collectibleKey + readonly property bool ensReady: d.ensDomainNameValid property int extendedDropdownType: ExtendedDropdownContent.Type.Assets @@ -139,8 +142,8 @@ StatusDropdown { function setDefaultAmounts() { d.assetAmountText = "" d.collectibleAmountText = "" - root.assetAmount = 0 - root.collectibleAmount = 1 + root.assetAmount = "0" + root.collectibleAmount = "1" } function forceLayout() { @@ -388,7 +391,8 @@ StatusDropdown { TokenPanel { id: assetPanel - readonly property real effectiveAmount: amountValid ? amount : 0 + readonly property string effectiveAmount: amountValid ? amount : "0" + property bool completed: false tokenName: PermissionsHelpers.getTokenNameByKey(root.assetsModel, root.assetKey) tokenShortName: PermissionsHelpers.getTokenShortNameByKey(root.assetsModel, root.assetKey) @@ -415,9 +419,10 @@ StatusDropdown { return append({ - name:chainName, + name: chainName, icon: chainIcon, amount: asset.supply, + multiplierIndex: asset.multiplierIndex, infiniteAmount: asset.infiniteSupply }) @@ -425,7 +430,12 @@ StatusDropdown { } } - onEffectiveAmountChanged: root.assetAmount = effectiveAmount + onEffectiveAmountChanged: { + if (completed) + root.assetAmount = effectiveAmount + } + + onMultiplierIndexChanged: root.assetMultiplierIndex = multiplierIndex onAmountTextChanged: d.assetAmountText = amountText onAddClicked: root.addAsset(root.assetKey, root.assetAmount) onUpdateClicked: root.updateAsset(root.assetKey, root.assetAmount) @@ -438,8 +448,11 @@ StatusDropdown { } Component.onCompleted: { - if (d.assetAmountText.length === 0 && root.assetAmount) - assetPanel.setAmount(root.assetAmount) + completed = true + + if (d.assetAmountText.length === 0 && root.assetAmount !== "0") + assetPanel.setAmount(root.assetAmount, + root.assetMultiplierIndex) } } } @@ -450,7 +463,8 @@ StatusDropdown { TokenPanel { id: collectiblePanel - readonly property real effectiveAmount: amountValid ? amount : 0 + readonly property string effectiveAmount: amountValid ? amount : "0" + property bool completed: false tokenName: PermissionsHelpers.getTokenNameByKey(root.collectiblesModel, root.collectibleKey) tokenShortName: "" @@ -482,6 +496,7 @@ StatusDropdown { name:chainName, icon: chainIcon, amount: collectible.supply, + multiplierIndex: collectible.multiplierIndex, infiniteAmount: collectible.infiniteSupply }) @@ -489,13 +504,19 @@ StatusDropdown { } } - onEffectiveAmountChanged: root.collectibleAmount = effectiveAmount + onEffectiveAmountChanged: { + if (completed) + root.collectibleAmount = effectiveAmount + } + onAmountTextChanged: d.collectibleAmountText = amountText onAddClicked: root.addCollectible(root.collectibleKey, root.collectibleAmount) onUpdateClicked: root.updateCollectible(root.collectibleKey, root.collectibleAmount) onRemoveClicked: root.removeClicked() Component.onCompleted: { + completed = true + if (d.collectibleAmountText.length === 0 && root.collectibleAmount) collectiblePanel.setAmount(root.collectibleAmount) } diff --git a/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml index 6b607facd4..aa3be99ae4 100644 --- a/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml +++ b/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml @@ -403,8 +403,8 @@ StatusSectionLayout { onAirdropToken: { root.goTo(Constants.CommunitySettingsSections.Airdrops) - // Force a token selection to be airdroped with default amount 1 - airdropPanel.selectToken(tokenKey, 1, type) + // Force a token selection to be airdroped with given amount + airdropPanel.selectToken(tokenKey, amount, type) // Set given addresses as recipients airdropPanel.addAddresses(addresses) diff --git a/ui/app/AppLayouts/Communities/views/EditAirdropView.qml b/ui/app/AppLayouts/Communities/views/EditAirdropView.qml index b20f93daa0..785022ec88 100644 --- a/ui/app/AppLayouts/Communities/views/EditAirdropView.qml +++ b/ui/app/AppLayouts/Communities/views/EditAirdropView.qml @@ -87,11 +87,9 @@ StatusScrollView { function selectToken(key, amount, type) { if(selectedHoldingsModel) selectedHoldingsModel.clear() - var tokenModel = null - if(type === Constants.TokenType.ERC20) - tokenModel = root.assetsModel - else if (type === Constants.TokenType.ERC721) - tokenModel = root.collectiblesModel + + const tokenModel = type === Constants.TokenType.ERC20 ? + root.assetsModel : root.collectiblesModel const modelItem = PermissionsHelpers.getTokenByKey( tokenModel, key) @@ -178,22 +176,23 @@ StatusScrollView { onTotalRevisionChanged: Qt.callLater(() => d.resetFees()) function prepareEntry(key, amount, type) { - let tokenModel = null - if(type === Constants.TokenType.ERC20) - tokenModel = root.assetsModel - else if (type === Constants.TokenType.ERC721) - tokenModel = root.collectiblesModel - + const tokenModel = type === Constants.TokenType.ERC20 + ? root.assetsModel : root.collectiblesModel const modelItem = PermissionsHelpers.getTokenByKey( tokenModel, key) - + const multiplierIndex = modelItem.multiplierIndex + const amountNumber = AmountsArithmetic.toNumber( + amount, multiplierIndex) + const amountLocalized = LocaleUtils.numberToLocaleString( + amountNumber, -1) return { key, amount, type, - tokenText: amount + " " + modelItem.name, + tokenText: amountLocalized + " " + modelItem.name, tokenImage: modelItem.iconSource, networkText: modelItem.chainName, networkImage: Style.svg(modelItem.chainIcon), supply: modelItem.supply, + multiplierIndex: modelItem.multiplierIndex, infiniteSupply: modelItem.infiniteSupply, contractUniqueKey: modelItem.contractUniqueKey, accountName: modelItem.accountName, @@ -278,7 +277,13 @@ StatusScrollView { if (!item || item.infiniteSupply) continue - min = Math.min(item.supply / item.amount, min) + const dividient = AmountsArithmetic.fromString(item.supply) + const divisor = AmountsArithmetic.fromString(item.amount) + + const quotient = AmountsArithmetic.toNumber( + AmountsArithmetic.div(dividient, divisor)) + + min = Math.min(quotient, min) } infinity = min === Number.MAX_SAFE_INTEGER @@ -286,12 +291,24 @@ StatusScrollView { } delegate: QtObject { - readonly property int supply: model.supply - readonly property real amount: model.amount + readonly property string supply: model.supply + readonly property string amount: model.amount readonly property bool infiniteSupply: model.infiniteSupply - readonly property bool valid: - infiniteSupply || amount * airdropRecipientsSelector.count <= supply + readonly property bool valid: { + if (infiniteSupply) + return true + + const recipientsCount = airdropRecipientsSelector.count + const demand = AmountsArithmetic.times( + AmountsArithmetic.fromString(amount), + recipientsCount) + + const available = AmountsArithmetic.fromString(supply) + + return AmountsArithmetic.cmp(demand, available) <= 0 + } + onSupplyChanged: recipientsCountInstantiator.findRecipientsCount() onAmountChanged: recipientsCountInstantiator.findRecipientsCount() @@ -426,6 +443,7 @@ StatusScrollView { case HoldingTypes.Type.Asset: dropdown.assetKey = modelItem.key dropdown.assetAmount = modelItem.amount + dropdown.assetMultiplierIndex = modelItem.multiplierIndex dropdown.setActiveTab(HoldingTypes.Type.Asset) break case HoldingTypes.Type.Collectible: diff --git a/ui/app/AppLayouts/Communities/views/EditCommunityTokenView.qml b/ui/app/AppLayouts/Communities/views/EditCommunityTokenView.qml index fe33c7f77b..05b065aaa4 100644 --- a/ui/app/AppLayouts/Communities/views/EditCommunityTokenView.qml +++ b/ui/app/AppLayouts/Communities/views/EditCommunityTokenView.qml @@ -254,7 +254,13 @@ StatusScrollView { visible: !unlimitedSupplyChecker.checked label: qsTr("Total finite supply") - text: root.isAssetView ? asset.supply : collectible.supply + text: { + const token = root.isAssetView ? root.asset : root.collectible + + return SQUtils.AmountsArithmetic.toNumber(token.supply, + token.multiplierIndex) + } + placeholderText: qsTr("e.g. 300") minLengthValidator.errorMessage: qsTr("Please enter a total finite supply") regexValidator.errorMessage: d.hasEmoji(text) ? qsTr("Your total finite supply is too cool (use 0-9 only)") : @@ -264,14 +270,13 @@ StatusScrollView { extraValidator.errorMessage: qsTr("Enter a number between 1 and 999,999,999") onTextChanged: { - const amount = parseInt(text) - if (Number.isNaN(amount) || Object.values(errors).length) + const supplyNumber = parseInt(text) + if (Number.isNaN(supplyNumber) || Object.values(errors).length) return - if(root.isAssetView) - asset.supply = amount - else - collectible.supply = amount + const token = root.isAssetView ? root.asset : root.collectible + token.supply = SQUtils.AmountsArithmetic.fromNumber( + supplyNumber, token.multiplierIndex).toFixed(0) } } diff --git a/ui/app/AppLayouts/Communities/views/EditOwnerTokenView.qml b/ui/app/AppLayouts/Communities/views/EditOwnerTokenView.qml index 2e3f28be23..3cd754a628 100644 --- a/ui/app/AppLayouts/Communities/views/EditOwnerTokenView.qml +++ b/ui/app/AppLayouts/Communities/views/EditOwnerTokenView.qml @@ -49,7 +49,7 @@ StatusScrollView { symbol: PermissionsHelpers.communityNameToSymbol(true, root.communityName) transferable: true remotelyDestruct: false - supply: 1 + supply: "1" infiniteSupply: false description: qsTr("This is the %1 Owner token. The hodler of this collectible has ultimate control over %1 Community token administration.").arg(root.communityName) } diff --git a/ui/app/AppLayouts/Communities/views/EditPermissionView.qml b/ui/app/AppLayouts/Communities/views/EditPermissionView.qml index 258f3536ac..c39dd68d59 100644 --- a/ui/app/AppLayouts/Communities/views/EditPermissionView.qml +++ b/ui/app/AppLayouts/Communities/views/EditPermissionView.qml @@ -172,7 +172,7 @@ StatusScrollView { onPermissionTypeChanged: Qt.callLater(() => d.loadInitValues()) contentWidth: mainLayout.width contentHeight: mainLayout.height - + SequenceColumnLayout { id: mainLayout @@ -242,7 +242,7 @@ StatusScrollView { const key = item.key d.dirtyValues.selectedHoldingsModel.append( - { type, key, amount }) + { type, key, amount: parseFloat(amount) }) } function prepareUpdateIndex(key) { @@ -270,6 +270,7 @@ StatusScrollView { onAddAsset: { const modelItem = PermissionsHelpers.getTokenByKey( root.assetsModel, key) + addItem(HoldingTypes.Type.Asset, modelItem, amount) dropdown.close() } @@ -277,6 +278,7 @@ StatusScrollView { onAddCollectible: { const modelItem = PermissionsHelpers.getTokenByKey( root.collectiblesModel, key) + addItem(HoldingTypes.Type.Collectible, modelItem, amount) dropdown.close() } @@ -292,7 +294,7 @@ StatusScrollView { const modelItem = PermissionsHelpers.getTokenByKey(root.assetsModel, key) d.dirtyValues.selectedHoldingsModel.set( - itemIndex, { type: HoldingTypes.Type.Asset, key, amount }) + itemIndex, { type: HoldingTypes.Type.Asset, key, amount: parseFloat(amount) }) dropdown.close() } @@ -303,7 +305,7 @@ StatusScrollView { d.dirtyValues.selectedHoldingsModel.set( itemIndex, - { type: HoldingTypes.Type.Collectible, key, amount }) + { type: HoldingTypes.Type.Collectible, key, amount: parseFloat(amount) }) dropdown.close() } diff --git a/ui/app/AppLayouts/Communities/views/MintedTokensView.qml b/ui/app/AppLayouts/Communities/views/MintedTokensView.qml index 20a13176d8..bfa8aa92fc 100644 --- a/ui/app/AppLayouts/Communities/views/MintedTokensView.qml +++ b/ui/app/AppLayouts/Communities/views/MintedTokensView.qml @@ -161,7 +161,7 @@ StatusScrollView { id: assetsList Layout.fillWidth: true - Layout.preferredHeight: childrenRect.height + Layout.preferredHeight: contentHeight visible: count > 0 model: assetsModel diff --git a/ui/imports/shared/controls/AmountInput.qml b/ui/imports/shared/controls/AmountInput.qml index f28b6fb7d1..ba9de3cf9e 100644 --- a/ui/imports/shared/controls/AmountInput.qml +++ b/ui/imports/shared/controls/AmountInput.qml @@ -3,6 +3,7 @@ import QtQuick.Layouts 1.14 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils import utils 1.0 @@ -13,11 +14,13 @@ Input { property var locale: Qt.locale() readonly property alias amount: d.amount + property alias multiplierIndex: d.multiplierIndex + readonly property bool valid: validationError.length === 0 property bool allowDecimals: true property bool validateMaximumAmount: false - property real maximumAmount: 0 + property string maximumAmount: "0" validationErrorTopMargin: 8 fontPixelSize: 13 @@ -27,18 +30,27 @@ Input { textField.rightPadding: labelText.implicitWidth + labelText.anchors.rightMargin + textField.leftPadding - function setAmount(amount) { - root.text = LocaleUtils.numberToLocaleString(amount, -1, root.locale) + function setAmount(amount, multiplierIndex = 0) { + console.assert(typeof amount === "string") + + d.multiplierIndex = multiplierIndex + const amountNumber = SQUtils.AmountsArithmetic.toNumber( + amount, multiplierIndex) + + root.text = LocaleUtils.numberToLocaleString(amountNumber, -1, + root.locale) } onTextChanged: d.validate() onValidateMaximumAmountChanged: d.validate() onMaximumAmountChanged: d.validate() + onMultiplierIndexChanged: d.validate() QtObject { id: d - property real amount: 0 + property string amount: "0" + property int multiplierIndex: 0 function getEffectiveDigitsCount(str) { const digits = LocaleUtils.getLocalizedDigitsCount(text, root.locale) @@ -50,7 +62,7 @@ Input { root.text = root.text.replace(root.locale.decimalPoint, "") if(root.text.length === 0) { - d.amount = 0 + d.amount = "0" root.validationError = "" return } @@ -60,16 +72,38 @@ Input { return } - const amount = LocaleUtils.numberFromLocaleString(root.text, root.locale) - if (isNaN(amount)) { - d.amount = 0 + const amountNumber = LocaleUtils.numberFromLocaleString(root.text, root.locale) + if (isNaN(amountNumber)) { + d.amount = "0" root.validationError = qsTr("Invalid amount format") - } else if (root.validateMaximumAmount && amount > root.maximumAmount) { - root.validationError = qsTr("Amount exceeds balance") - } else { - d.amount = amount - root.validationError = "" + return } + + const amount = SQUtils.AmountsArithmetic.fromNumber( + amountNumber, d.multiplierIndex) + + if (root.validateMaximumAmount) { + const maximumAmount = SQUtils.AmountsArithmetic.fromString( + root.maximumAmount) + + const maxExceeded = SQUtils.AmountsArithmetic.cmp( + amount, maximumAmount) === 1 + + if (SQUtils.AmountsArithmetic.cmp(amount, maximumAmount) === 1) { + root.validationError = qsTr("Amount exceeds balance") + return + } + } + + // Fallback to handle float amounts for permissions + // As a target amount should be always integer number + if (!Number.isInteger(amountNumber) && d.multiplierIndex === 0) { + d.amount = amount.toString() + } else { + d.amount = amount.toFixed(0) + } + + root.validationError = "" } } diff --git a/ui/imports/shared/stores/CommunityTokensStore.qml b/ui/imports/shared/stores/CommunityTokensStore.qml index 50a72b6daf..92881c0215 100644 --- a/ui/imports/shared/stores/CommunityTokensStore.qml +++ b/ui/imports/shared/stores/CommunityTokensStore.qml @@ -26,10 +26,10 @@ QtObject { // Minting tokens: function deployCollectible(communityId, collectibleItem) - { - if (collectibleItem.key !== "") { + { + if (collectibleItem.key !== "") deleteToken(communityId, collectibleItem.key) - } + const jsonArtworkFile = Utils.getImageAndCropInfoJson(collectibleItem.artworkSource, collectibleItem.artworkCropRect) communityTokensModuleInst.deployCollectible(communityId, collectibleItem.accountAddress, collectibleItem.name, collectibleItem.symbol, collectibleItem.description, collectibleItem.supply, @@ -39,9 +39,9 @@ QtObject { function deployAsset(communityId, assetItem) { - if (assetItem.key !== "") { + if (assetItem.key !== "") deleteToken(communityId, assetItem.key) - } + const jsonArtworkFile = Utils.getImageAndCropInfoJson(assetItem.artworkSource, assetItem.artworkCropRect) communityTokensModuleInst.deployAssets(communityId, assetItem.accountAddress, assetItem.name, assetItem.symbol, assetItem.description, assetItem.supply, @@ -120,11 +120,13 @@ QtObject { // Burn: function computeBurnFee(tokenKey, amount, accountAddress) { + console.assert(typeof amount === "string") // TODO: Backend. It should include the account address in the calculation. communityTokensModuleInst.computeBurnFee(tokenKey, amount/*, accountAddress*/) } function burnToken(communityId, tokenKey, burnAmount, accountAddress) { + console.assert(typeof burnAmount === "string") // TODO: Backend. It should include the account address in the burn action. communityTokensModuleInst.burnTokens(communityId, tokenKey, burnAmount/*, accountAddress*/) }