From f00493ec0281bda15f83c0e6bbce3428924e248d Mon Sep 17 00:00:00 2001 From: Michal Iskierko Date: Tue, 19 Dec 2023 09:55:47 +0100 Subject: [PATCH] fix(@desktop/communities): Fix displaying token holders Add displaying holders for ERC20 - only community members. Add json conversions test for some holders structs. Fix #12062 --- .../tokens/models/token_owners_item.nim | 4 +- .../tokens/models/token_owners_model.nim | 4 +- .../service/community_tokens/async_tasks.nim | 83 ++++++++++++++++++- .../community_collectible_owner.nim | 14 +++- .../service/community_tokens/service.nim | 78 +++++++---------- src/backend/backend.nim | 5 ++ src/backend/collectibles_types.nim | 23 +++-- .../collectibles_types_conversions_test.nim | 43 ++++++++++ .../controls/TokenHolderListItem.qml | 6 +- .../panels/SortableTokenHoldersList.qml | 4 +- .../panels/SortableTokenHoldersPanel.qml | 2 + .../Communities/views/CommunityTokenView.qml | 2 + 12 files changed, 204 insertions(+), 64 deletions(-) create mode 100644 test/nim/collectibles_types_conversions_test.nim diff --git a/src/app/modules/main/communities/tokens/models/token_owners_item.nim b/src/app/modules/main/communities/tokens/models/token_owners_item.nim index 26452acf62..d15a68d3dd 100644 --- a/src/app/modules/main/communities/tokens/models/token_owners_item.nim +++ b/src/app/modules/main/communities/tokens/models/token_owners_item.nim @@ -9,7 +9,7 @@ type imageSource*: string numberOfMessages*: int ownerDetails*: CollectibleOwner - amount*: int + amount*: Uint256 remotelyDestructState*: ContractTransactionStatus proc remoteDestructTransactionStatus*(remoteDestructedAddresses: seq[string], address: string): ContractTransactionStatus = @@ -32,7 +32,7 @@ proc initTokenOwnersItem*( result.ownerDetails = ownerDetails result.remotelyDestructState = remoteDestructTransactionStatus(remoteDestructedAddresses, ownerDetails.address) for balance in ownerDetails.balances: - result.amount = result.amount + balance.balance.truncate(int) + result.amount = result.amount + balance.balance proc `$`*(self: TokenOwnersItem): string = result = fmt"""TokenOwnersItem( diff --git a/src/app/modules/main/communities/tokens/models/token_owners_model.nim b/src/app/modules/main/communities/tokens/models/token_owners_model.nim index 2ae0b97007..42c7d0a7d7 100644 --- a/src/app/modules/main/communities/tokens/models/token_owners_model.nim +++ b/src/app/modules/main/communities/tokens/models/token_owners_model.nim @@ -1,4 +1,4 @@ -import NimQml, Tables, strformat +import NimQml, Tables, strformat, stint import token_owners_item type @@ -83,7 +83,7 @@ QtObject: of ModelRole.WalletAddress: result = newQVariant(item.ownerDetails.address) of ModelRole.Amount: - result = newQVariant(item.amount) + result = newQVariant(item.amount.toString(10)) of ModelRole.RemotelyDestructState: result = newQVariant(item.remotelyDestructState.int) diff --git a/src/app_service/service/community_tokens/async_tasks.nim b/src/app_service/service/community_tokens/async_tasks.nim index 5dc8267e8f..dfac8f893b 100644 --- a/src/app_service/service/community_tokens/async_tasks.nim +++ b/src/app_service/service/community_tokens/async_tasks.nim @@ -17,6 +17,15 @@ proc tableToJsonArray[A, B](t: var Table[A, B]): JsonNode = }) return data +proc balanceInfoToTable(jsonNode: JsonNode): Table[string, UInt256] = + for chainBalancesPair in jsonNode.pairs(): + for addressTokenBalancesPair in chainBalancesPair.val.pairs(): + for tokenBalancesPair in addressTokenBalancesPair.val.pairs(): + let amount = fromHex(UInt256, tokenBalancesPair.val.getStr) + if amount != stint.u256(0): + result[addressTokenBalancesPair.key.toUpper] = amount + break + type AsyncDeployOwnerContractsFeesArg = ref object of QObjectTaskArg chainId: int @@ -227,16 +236,33 @@ type const fetchCollectibleOwnersTaskArg: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = let arg = decode[FetchCollectibleOwnersArg](argEncoded) try: - let response = collectibles.getCollectibleOwnersByContractAddress(arg.chainId, arg.contractAddress) + var response = collectibles.getCollectibleOwnersByContractAddress(arg.chainId, arg.contractAddress) - if not response.error.isNil: - raise newException(ValueError, "Error getCollectibleOwnersByContractAddress" & response.error.message) + var owners = fromJson(response.result, CollectibleContractOwnership).owners + owners = owners.filter(x => x.address != ZERO_ADDRESS) + + response = communities_backend.getCommunityMembersForWalletAddresses(arg.communityId, arg.chainId) + + let communityCollectibleOwners = owners.map(proc(owner: CollectibleOwner): CommunityCollectibleOwner = + let ownerAddressUp = owner.address.toUpper() + for responseAddress in response.result.keys(): + let responseAddressUp = responseAddress.toUpper() + if ownerAddressUp == responseAddressUp: + let member = response.result[responseAddress].toContactsDto() + return CommunityCollectibleOwner( + contactId: member.id, + name: member.displayName, + imageSource: member.image.thumbnail, + collectibleOwner: owner + ) + return CommunityCollectibleOwner(collectibleOwner: owner) + ) let output = %* { "chainId": arg.chainId, "contractAddress": arg.contractAddress, "communityId": arg.communityId, - "result": response.result, + "result": %communityCollectibleOwners, "error": "" } arg.finish(output) @@ -250,6 +276,55 @@ const fetchCollectibleOwnersTaskArg: Task = proc(argEncoded: string) {.gcsafe, n } arg.finish(output) +type + FetchAssetOwnersArg = ref object of QObjectTaskArg + chainId*: int + contractAddress*: string + communityId*: string + +const fetchAssetOwnersTaskArg: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[FetchAssetOwnersArg](argEncoded) + try: + let addressesResponse = communities_backend.getCommunityMembersForWalletAddresses(arg.communityId, arg.chainId) + var allCommunityMembersAddresses: seq[string] = @[] + for address in addressesResponse.result.keys(): + allCommunityMembersAddresses.add(address) + + let balancesResponse = backend.getBalancesByChain(@[arg.chainId], allCommunityMembersAddresses, @[arg.contractAddress]) + + let walletBalanceTable = balanceInfoToTable(balancesResponse.result) + + var collectibleOwners: seq[CommunityCollectibleOwner] = @[] + for wallet, balance in walletBalanceTable.pairs(): + let member = addressesResponse.result[wallet].toContactsDto() + let collectibleBalance = CollectibleBalance(tokenId: stint.u256(0), balance: balance) + let collectibleOwner = CollectibleOwner(address: wallet, balances: @[collectibleBalance]) + collectibleOwners.add(CommunityCollectibleOwner( + contactId: member.id, + name: member.displayName, + imageSource: member.image.thumbnail, + collectibleOwner: collectibleOwner + )) + + let output = %* { + "chainId": arg.chainId, + "contractAddress": arg.contractAddress, + "communityId": arg.communityId, + "result": %collectibleOwners, + "error": "" + } + arg.finish(output) + except Exception as e: + echo "Exception", e.msg + let output = %* { + "chainId": arg.chainId, + "contractAddress": arg.contractAddress, + "communityId": arg.communityId, + "result": "", + "error": e.msg + } + arg.finish(output) + type GetCommunityTokensDetailsArg = ref object of QObjectTaskArg communityId*: string diff --git a/src/app_service/service/community_tokens/community_collectible_owner.nim b/src/app_service/service/community_tokens/community_collectible_owner.nim index 5217e95ecd..37663e19ea 100644 --- a/src/app_service/service/community_tokens/community_collectible_owner.nim +++ b/src/app_service/service/community_tokens/community_collectible_owner.nim @@ -1,4 +1,5 @@ -from backend/collectibles_types import CollectibleOwner +import json +import backend/collectibles_types type CommunityCollectibleOwner* = object @@ -6,3 +7,14 @@ type name*: string imageSource*: string collectibleOwner*: CollectibleOwner + +proc toCommunityCollectibleOwners*(jsonAsset: JsonNode): seq[CommunityCollectibleOwner] = + var ownerList: seq[CommunityCollectibleOwner] = @[] + for item in jsonAsset.items: + ownerList.add(CommunityCollectibleOwner( + contactId: item{"contactId"}.getStr, + name: item{"name"}.getStr, + imageSource: item{"imageSource"}.getStr, + collectibleOwner: getCollectibleOwner(item{"collectibleOwner"}) + )) + return ownerList diff --git a/src/app_service/service/community_tokens/service.nim b/src/app_service/service/community_tokens/service.nim index 03be840f30..2a501351f5 100644 --- a/src/app_service/service/community_tokens/service.nim +++ b/src/app_service/service/community_tokens/service.nim @@ -1230,61 +1230,47 @@ QtObject: self.events.emit(SIGNAL_COMPUTE_AIRDROP_FEE, dataToEmit) proc fetchCommunityOwners*(self: Service, communityToken: CommunityTokenDto) = - if communityToken.tokenType != TokenType.ERC721: - # TODO we need a new implementation for ERC20 - # we will be able to show only tokens hold by community members + if communityToken.tokenType == TokenType.ERC20: + let arg = FetchAssetOwnersArg( + tptr: cast[ByteAddress](fetchAssetOwnersTaskArg), + vptr: cast[ByteAddress](self.vptr), + slot: "onCommunityTokenOwnersFetched", + chainId: communityToken.chainId, + contractAddress: communityToken.address, + communityId: communityToken.communityId + ) + self.threadpool.start(arg) + return + elif communityToken.tokenType == TokenType.ERC721: + let arg = FetchCollectibleOwnersArg( + tptr: cast[ByteAddress](fetchCollectibleOwnersTaskArg), + vptr: cast[ByteAddress](self.vptr), + slot: "onCommunityTokenOwnersFetched", + chainId: communityToken.chainId, + contractAddress: communityToken.address, + communityId: communityToken.communityId + ) + self.threadpool.start(arg) return - let arg = FetchCollectibleOwnersArg( - tptr: cast[ByteAddress](fetchCollectibleOwnersTaskArg), - vptr: cast[ByteAddress](self.vptr), - slot: "onCommunityTokenOwnersFetched", - chainId: communityToken.chainId, - contractAddress: communityToken.address, - communityId: communityToken.communityId - ) - self.threadpool.start(arg) - - # get owners from cache - proc getCommunityTokenOwners*(self: Service, communityId: string, chainId: int, contractAddress: string): seq[CommunityCollectibleOwner] = - return self.tokenOwnersCache.getOrDefault((chainId: chainId, address: contractAddress)) proc onCommunityTokenOwnersFetched*(self:Service, response: string) {.slot.} = let responseJson = response.parseJson() if responseJson{"error"}.kind != JNull and responseJson{"error"}.getStr != "": let errorMessage = responseJson["error"].getStr - error "Can't fetch community token owners", chainId=responseJson["chainId"], contractAddress=responseJson["contractAddress"], errorMsg=errorMessage + error "Can't fetch community token owners", chainId=responseJson{"chainId"}, contractAddress=responseJson{"contractAddress"}, errorMsg=errorMessage return - let chainId = responseJson["chainId"].getInt - let contractAddress = responseJson["contractAddress"].getStr - let communityId = responseJson["communityId"].getStr - let resultJson = responseJson["result"] - var owners = fromJson(resultJson, CollectibleContractOwnership).owners - owners = owners.filter(x => x.address != ZERO_ADDRESS) - - let response = communities_backend.getCommunityMembersForWalletAddresses(communityId, chainId) - if response.error != nil: - let errorMessage = responseJson["error"].getStr - error "Can't get community members with addresses", errorMsg=errorMessage - return - - let communityOwners = owners.map(proc(owner: CollectibleOwner): CommunityCollectibleOwner = - let ownerAddressUp = owner.address.toUpper() - for responseAddress in response.result.keys(): - let responseAddressUp = responseAddress.toUpper() - if ownerAddressUp == responseAddressUp: - let member = response.result[responseAddress].toContactsDto() - return CommunityCollectibleOwner( - contactId: member.id, - name: member.displayName, - imageSource: member.image.thumbnail, - collectibleOwner: owner - ) - return CommunityCollectibleOwner(collectibleOwner: owner) - ) - self.tokenOwnersCache[(chainId, contractAddress)] = communityOwners - let data = CommunityTokenOwnersArgs(chainId: chainId, contractAddress: contractAddress, communityId: communityId, owners: communityOwners) + let chainId = responseJson{"chainId"}.getInt + let contractAddress = responseJson{"contractAddress"}.getStr + let communityId = responseJson{"communityId"}.getStr + let communityTokenOwners = toCommunityCollectibleOwners(responseJson{"result"}) + self.tokenOwnersCache[(chainId, contractAddress)] = communityTokenOwners + let data = CommunityTokenOwnersArgs(chainId: chainId, contractAddress: contractAddress, communityId: communityId, owners: communityTokenOwners) self.events.emit(SIGNAL_COMMUNITY_TOKEN_OWNERS_FETCHED, data) + # get owners from cache + proc getCommunityTokenOwners*(self: Service, communityId: string, chainId: int, contractAddress: string): seq[CommunityCollectibleOwner] = + return self.tokenOwnersCache.getOrDefault((chainId: chainId, address: contractAddress)) + proc onRefreshTransferableTokenOwners*(self:Service) {.slot.} = let allTokens = self.getAllCommunityTokens() for token in allTokens: diff --git a/src/backend/backend.nim b/src/backend/backend.nim index 1af8e46ecf..a014efe344 100644 --- a/src/backend/backend.nim +++ b/src/backend/backend.nim @@ -322,3 +322,8 @@ rpc(fetchAllCurrencyFormats, "wallet"): rpc(hasPairedDevices, "accounts"): discard + +rpc(getBalancesByChain, "wallet"): + chainIds: seq[int] + addresses: seq[string] + tokenAddresses: seq[string] diff --git a/src/backend/collectibles_types.nim b/src/backend/collectibles_types.nim index 118662d0be..0eeb85ddd4 100644 --- a/src/backend/collectibles_types.nim +++ b/src/backend/collectibles_types.nim @@ -93,6 +93,11 @@ proc `%`*(t: ContractID): JsonNode {.inline.} = proc `%`*(t: ref ContractID): JsonNode {.inline.} = return %(t[]) +proc `%`*(self: CollectibleBalance): JsonNode {.inline.} = + result = newJObject() + result["tokenId"] = %self.tokenId.toString() + result["balance"] = %self.balance.toString() + proc fromJson*(t: JsonNode, T: typedesc[ContractID]): ContractID {.inline.} = result = ContractID() result.chainID = t["chainID"].getInt() @@ -316,7 +321,7 @@ proc `$`*(self: CollectibleBalance): string = balance:{self.balance} """ -proc getCollectibleBalances(jsonAsset: JsonNode): seq[CollectibleBalance] = +proc getCollectibleBalances*(jsonAsset: JsonNode): seq[CollectibleBalance] = var balanceList: seq[CollectibleBalance] = @[] for item in jsonAsset.items: balanceList.add(CollectibleBalance( @@ -332,13 +337,21 @@ proc `$`*(self: CollectibleOwner): string = balances:{self.balances} """ +proc getCollectibleOwner*(jsonAsset: JsonNode): CollectibleOwner = + return CollectibleOwner( + address: jsonAsset{"ownerAddress"}.getStr, + balances: getCollectibleBalances(jsonAsset{"tokenBalances"}) + ) + +proc `%`*(self: CollectibleOwner): JsonNode {.inline.} = + result = newJObject() + result["ownerAddress"] = %(self.address) + result["tokenBalances"] = %(self.balances) + proc getCollectibleOwners(jsonAsset: JsonNode): seq[CollectibleOwner] = var ownerList: seq[CollectibleOwner] = @[] for item in jsonAsset.items: - ownerList.add(CollectibleOwner( - address: item{"ownerAddress"}.getStr, - balances: getCollectibleBalances(item{"tokenBalances"}) - )) + ownerList.add(getCollectibleOwner(item)) return ownerList # CollectibleContractOwnership diff --git a/test/nim/collectibles_types_conversions_test.nim b/test/nim/collectibles_types_conversions_test.nim new file mode 100644 index 0000000000..5fe8eb0663 --- /dev/null +++ b/test/nim/collectibles_types_conversions_test.nim @@ -0,0 +1,43 @@ +import unittest + +import stint +import backend/collectibles_types +import app_service/service/community_tokens/community_collectible_owner +include app_service/common/json_utils + +suite "collectibles types": + + test "CollectibleOwner json conversion": + + let oldBalance1 = CollectibleBalance(tokenId: stint.u256(23), balance: stint.u256(41)) + let oldBalance2 = CollectibleBalance(tokenId: stint.u256(24), balance: stint.u256(123456789123456789)) + let oldBalances = @[oldBalance1, oldBalance2] + let oldOwner = CollectibleOwner(address: "abc", balances: oldBalances) + + let ownerJson = %oldOwner + + let newOwner = getCollectibleOwner(ownerJson) + + check(oldOwner.address == newOwner.address) + check(oldOwner.balances.len == newOwner.balances.len) + check(oldOwner.balances[0].tokenId == newOwner.balances[0].tokenId) + check(oldOwner.balances[0].balance == newOwner.balances[0].balance) + check(oldOwner.balances[1].tokenId == newOwner.balances[1].tokenId) + check(oldOwner.balances[1].balance == newOwner.balances[1].balance) + + test "CommunityCollectibleOwner json conversion": + + let oldBalance = CollectibleBalance(tokenId: stint.u256(23), balance: stint.u256(41)) + let oldCollOwner = CollectibleOwner(address: "abc", balances: @[oldBalance]) + let oldCommOwner = CommunityCollectibleOwner(contactId: "id1", name: "abc", imageSource: "xyz", collectibleOwner: oldCollOwner) + + let oldCommOwners = @[oldCommOwner] + let commOwnersJson = %(oldCommOwners) + + let newCommOwners = toCommunityCollectibleOwners(commOwnersJson) + + check(oldCommOwners.len == newCommOwners.len) + check(oldCommOwners[0].contactId == newCommOwners[0].contactId) + check(oldCommOwners[0].name == newCommOwners[0].name) + check(oldCommOwners[0].imageSource == newCommOwners[0].imageSource) + check(oldCommOwners[0].collectibleOwner.address == newCommOwners[0].collectibleOwner.address) diff --git a/ui/app/AppLayouts/Communities/controls/TokenHolderListItem.qml b/ui/app/AppLayouts/Communities/controls/TokenHolderListItem.qml index 5274238261..c39ebc0bbb 100644 --- a/ui/app/AppLayouts/Communities/controls/TokenHolderListItem.qml +++ b/ui/app/AppLayouts/Communities/controls/TokenHolderListItem.qml @@ -32,7 +32,7 @@ ItemDelegate { property string walletAddress property string imageSource property int numberOfMessages: 0 - property int amount: 0 + property string amount: "0" property var contactDetails: null @@ -109,7 +109,7 @@ ItemDelegate { pubKey: root.contactDetails.isEnsVerified ? "" : Utils.getCompressedPk(root.contactId) isContact: root.contactDetails.isContact isVerified: root.contactDetails.verificationStatus === Constants.verificationStatus.verified - isUntrustworthy: root.contactDetails.trustStatus == Constants.trustStatus.untrustworthy + isUntrustworthy: root.contactDetails.trustStatus === Constants.trustStatus.untrustworthy isAdmin: root.contactDetails.memberRole === Constants.memberRole.owner status: root.contactDetails.onlineStatus asset.name: root.contactDetails.displayIcon @@ -178,7 +178,7 @@ ItemDelegate { TokenHolderNumberCell { Layout.alignment: Qt.AlignRight - text: LocaleUtils.numberToLocaleString(root.amount) + text: root.amount } StatusLoadingIndicator { diff --git a/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersList.qml b/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersList.qml index cce7b3532f..1f489fc4f6 100644 --- a/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersList.qml +++ b/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersList.qml @@ -22,6 +22,8 @@ import "../controls" StatusListView { id: root + property int multiplierIndex: 0 + readonly property alias sortBy: d.sortBy readonly property alias sortOrder: d.sorting @@ -157,7 +159,7 @@ StatusListView { walletAddress: model.walletAddress imageSource: model.imageSource numberOfMessages: model.numberOfMessages - amount: model.amount + amount: LocaleUtils.numberToLocaleString(StatusQUtils.AmountsArithmetic.toNumber(model.amount, root.multiplierIndex)) showSeparator: isFirstRowAddress && root.sortBy === TokenHoldersProxyModel.SortBy.Username isFirstRowAddress: { diff --git a/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersPanel.qml b/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersPanel.qml index 8cdb294b97..88eb3138ec 100644 --- a/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersPanel.qml @@ -21,6 +21,7 @@ Control { property string tokenName property bool showRemotelyDestructMenuItem: true property alias isAirdropEnabled: infoBoxPanel.buttonEnabled + property int multiplierIndex: 0 readonly property alias sortBy: holdersList.sortBy readonly property alias sorting: holdersList.sortOrder @@ -121,6 +122,7 @@ Control { id: holdersList visible: !root.empty && proxyModel.count > 0 + multiplierIndex: root.multiplierIndex Layout.fillWidth: true Layout.preferredHeight: contentHeight diff --git a/ui/app/AppLayouts/Communities/views/CommunityTokenView.qml b/ui/app/AppLayouts/Communities/views/CommunityTokenView.qml index 5b6e252987..e52fcf2c3a 100644 --- a/ui/app/AppLayouts/Communities/views/CommunityTokenView.qml +++ b/ui/app/AppLayouts/Communities/views/CommunityTokenView.qml @@ -49,6 +49,7 @@ StatusScrollView { readonly property bool transferable: token.transferable readonly property string chainIcon: token.chainIcon readonly property int decimals: token.decimals + readonly property int multiplierIndex: token.multiplierIndex readonly property bool deploymentCompleted: deployState === Constants.ContractTransactionStatus.Completed @@ -203,6 +204,7 @@ StatusScrollView { showRemotelyDestructMenuItem: !root.isAssetView && root.remotelyDestruct isAirdropEnabled: root.deploymentCompleted && (token.infiniteSupply || token.remainingTokens > 0) + multiplierIndex: root.multiplierIndex Layout.topMargin: Style.current.padding Layout.fillWidth: true