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
This commit is contained in:
Michal Iskierko 2023-12-19 09:55:47 +01:00 committed by Michał Iskierko
parent 9410de4286
commit f00493ec02
12 changed files with 204 additions and 64 deletions

View File

@ -9,7 +9,7 @@ type
imageSource*: string imageSource*: string
numberOfMessages*: int numberOfMessages*: int
ownerDetails*: CollectibleOwner ownerDetails*: CollectibleOwner
amount*: int amount*: Uint256
remotelyDestructState*: ContractTransactionStatus remotelyDestructState*: ContractTransactionStatus
proc remoteDestructTransactionStatus*(remoteDestructedAddresses: seq[string], address: string): ContractTransactionStatus = proc remoteDestructTransactionStatus*(remoteDestructedAddresses: seq[string], address: string): ContractTransactionStatus =
@ -32,7 +32,7 @@ proc initTokenOwnersItem*(
result.ownerDetails = ownerDetails result.ownerDetails = ownerDetails
result.remotelyDestructState = remoteDestructTransactionStatus(remoteDestructedAddresses, ownerDetails.address) result.remotelyDestructState = remoteDestructTransactionStatus(remoteDestructedAddresses, ownerDetails.address)
for balance in ownerDetails.balances: for balance in ownerDetails.balances:
result.amount = result.amount + balance.balance.truncate(int) result.amount = result.amount + balance.balance
proc `$`*(self: TokenOwnersItem): string = proc `$`*(self: TokenOwnersItem): string =
result = fmt"""TokenOwnersItem( result = fmt"""TokenOwnersItem(

View File

@ -1,4 +1,4 @@
import NimQml, Tables, strformat import NimQml, Tables, strformat, stint
import token_owners_item import token_owners_item
type type
@ -83,7 +83,7 @@ QtObject:
of ModelRole.WalletAddress: of ModelRole.WalletAddress:
result = newQVariant(item.ownerDetails.address) result = newQVariant(item.ownerDetails.address)
of ModelRole.Amount: of ModelRole.Amount:
result = newQVariant(item.amount) result = newQVariant(item.amount.toString(10))
of ModelRole.RemotelyDestructState: of ModelRole.RemotelyDestructState:
result = newQVariant(item.remotelyDestructState.int) result = newQVariant(item.remotelyDestructState.int)

View File

@ -17,6 +17,15 @@ proc tableToJsonArray[A, B](t: var Table[A, B]): JsonNode =
}) })
return data 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 type
AsyncDeployOwnerContractsFeesArg = ref object of QObjectTaskArg AsyncDeployOwnerContractsFeesArg = ref object of QObjectTaskArg
chainId: int chainId: int
@ -227,16 +236,33 @@ type
const fetchCollectibleOwnersTaskArg: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = const fetchCollectibleOwnersTaskArg: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[FetchCollectibleOwnersArg](argEncoded) let arg = decode[FetchCollectibleOwnersArg](argEncoded)
try: try:
let response = collectibles.getCollectibleOwnersByContractAddress(arg.chainId, arg.contractAddress) var response = collectibles.getCollectibleOwnersByContractAddress(arg.chainId, arg.contractAddress)
if not response.error.isNil: var owners = fromJson(response.result, CollectibleContractOwnership).owners
raise newException(ValueError, "Error getCollectibleOwnersByContractAddress" & response.error.message) 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 = %* { let output = %* {
"chainId": arg.chainId, "chainId": arg.chainId,
"contractAddress": arg.contractAddress, "contractAddress": arg.contractAddress,
"communityId": arg.communityId, "communityId": arg.communityId,
"result": response.result, "result": %communityCollectibleOwners,
"error": "" "error": ""
} }
arg.finish(output) arg.finish(output)
@ -250,6 +276,55 @@ const fetchCollectibleOwnersTaskArg: Task = proc(argEncoded: string) {.gcsafe, n
} }
arg.finish(output) 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 type
GetCommunityTokensDetailsArg = ref object of QObjectTaskArg GetCommunityTokensDetailsArg = ref object of QObjectTaskArg
communityId*: string communityId*: string

View File

@ -1,4 +1,5 @@
from backend/collectibles_types import CollectibleOwner import json
import backend/collectibles_types
type type
CommunityCollectibleOwner* = object CommunityCollectibleOwner* = object
@ -6,3 +7,14 @@ type
name*: string name*: string
imageSource*: string imageSource*: string
collectibleOwner*: CollectibleOwner 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

View File

@ -1230,61 +1230,47 @@ QtObject:
self.events.emit(SIGNAL_COMPUTE_AIRDROP_FEE, dataToEmit) self.events.emit(SIGNAL_COMPUTE_AIRDROP_FEE, dataToEmit)
proc fetchCommunityOwners*(self: Service, communityToken: CommunityTokenDto) = proc fetchCommunityOwners*(self: Service, communityToken: CommunityTokenDto) =
if communityToken.tokenType != TokenType.ERC721: if communityToken.tokenType == TokenType.ERC20:
# TODO we need a new implementation for ERC20 let arg = FetchAssetOwnersArg(
# we will be able to show only tokens hold by community members 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 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.} = proc onCommunityTokenOwnersFetched*(self:Service, response: string) {.slot.} =
let responseJson = response.parseJson() let responseJson = response.parseJson()
if responseJson{"error"}.kind != JNull and responseJson{"error"}.getStr != "": if responseJson{"error"}.kind != JNull and responseJson{"error"}.getStr != "":
let errorMessage = 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 return
let chainId = responseJson["chainId"].getInt let chainId = responseJson{"chainId"}.getInt
let contractAddress = responseJson["contractAddress"].getStr let contractAddress = responseJson{"contractAddress"}.getStr
let communityId = responseJson["communityId"].getStr let communityId = responseJson{"communityId"}.getStr
let resultJson = responseJson["result"] let communityTokenOwners = toCommunityCollectibleOwners(responseJson{"result"})
var owners = fromJson(resultJson, CollectibleContractOwnership).owners self.tokenOwnersCache[(chainId, contractAddress)] = communityTokenOwners
owners = owners.filter(x => x.address != ZERO_ADDRESS) let data = CommunityTokenOwnersArgs(chainId: chainId, contractAddress: contractAddress, communityId: communityId, owners: communityTokenOwners)
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)
self.events.emit(SIGNAL_COMMUNITY_TOKEN_OWNERS_FETCHED, data) 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.} = proc onRefreshTransferableTokenOwners*(self:Service) {.slot.} =
let allTokens = self.getAllCommunityTokens() let allTokens = self.getAllCommunityTokens()
for token in allTokens: for token in allTokens:

View File

@ -322,3 +322,8 @@ rpc(fetchAllCurrencyFormats, "wallet"):
rpc(hasPairedDevices, "accounts"): rpc(hasPairedDevices, "accounts"):
discard discard
rpc(getBalancesByChain, "wallet"):
chainIds: seq[int]
addresses: seq[string]
tokenAddresses: seq[string]

View File

@ -93,6 +93,11 @@ proc `%`*(t: ContractID): JsonNode {.inline.} =
proc `%`*(t: ref ContractID): JsonNode {.inline.} = proc `%`*(t: ref ContractID): JsonNode {.inline.} =
return %(t[]) 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.} = proc fromJson*(t: JsonNode, T: typedesc[ContractID]): ContractID {.inline.} =
result = ContractID() result = ContractID()
result.chainID = t["chainID"].getInt() result.chainID = t["chainID"].getInt()
@ -316,7 +321,7 @@ proc `$`*(self: CollectibleBalance): string =
balance:{self.balance} balance:{self.balance}
""" """
proc getCollectibleBalances(jsonAsset: JsonNode): seq[CollectibleBalance] = proc getCollectibleBalances*(jsonAsset: JsonNode): seq[CollectibleBalance] =
var balanceList: seq[CollectibleBalance] = @[] var balanceList: seq[CollectibleBalance] = @[]
for item in jsonAsset.items: for item in jsonAsset.items:
balanceList.add(CollectibleBalance( balanceList.add(CollectibleBalance(
@ -332,13 +337,21 @@ proc `$`*(self: CollectibleOwner): string =
balances:{self.balances} 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] = proc getCollectibleOwners(jsonAsset: JsonNode): seq[CollectibleOwner] =
var ownerList: seq[CollectibleOwner] = @[] var ownerList: seq[CollectibleOwner] = @[]
for item in jsonAsset.items: for item in jsonAsset.items:
ownerList.add(CollectibleOwner( ownerList.add(getCollectibleOwner(item))
address: item{"ownerAddress"}.getStr,
balances: getCollectibleBalances(item{"tokenBalances"})
))
return ownerList return ownerList
# CollectibleContractOwnership # CollectibleContractOwnership

View File

@ -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)

View File

@ -32,7 +32,7 @@ ItemDelegate {
property string walletAddress property string walletAddress
property string imageSource property string imageSource
property int numberOfMessages: 0 property int numberOfMessages: 0
property int amount: 0 property string amount: "0"
property var contactDetails: null property var contactDetails: null
@ -109,7 +109,7 @@ ItemDelegate {
pubKey: root.contactDetails.isEnsVerified ? "" : Utils.getCompressedPk(root.contactId) pubKey: root.contactDetails.isEnsVerified ? "" : Utils.getCompressedPk(root.contactId)
isContact: root.contactDetails.isContact isContact: root.contactDetails.isContact
isVerified: root.contactDetails.verificationStatus === Constants.verificationStatus.verified 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 isAdmin: root.contactDetails.memberRole === Constants.memberRole.owner
status: root.contactDetails.onlineStatus status: root.contactDetails.onlineStatus
asset.name: root.contactDetails.displayIcon asset.name: root.contactDetails.displayIcon
@ -178,7 +178,7 @@ ItemDelegate {
TokenHolderNumberCell { TokenHolderNumberCell {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
text: LocaleUtils.numberToLocaleString(root.amount) text: root.amount
} }
StatusLoadingIndicator { StatusLoadingIndicator {

View File

@ -22,6 +22,8 @@ import "../controls"
StatusListView { StatusListView {
id: root id: root
property int multiplierIndex: 0
readonly property alias sortBy: d.sortBy readonly property alias sortBy: d.sortBy
readonly property alias sortOrder: d.sorting readonly property alias sortOrder: d.sorting
@ -157,7 +159,7 @@ StatusListView {
walletAddress: model.walletAddress walletAddress: model.walletAddress
imageSource: model.imageSource imageSource: model.imageSource
numberOfMessages: model.numberOfMessages numberOfMessages: model.numberOfMessages
amount: model.amount amount: LocaleUtils.numberToLocaleString(StatusQUtils.AmountsArithmetic.toNumber(model.amount, root.multiplierIndex))
showSeparator: isFirstRowAddress && root.sortBy === TokenHoldersProxyModel.SortBy.Username showSeparator: isFirstRowAddress && root.sortBy === TokenHoldersProxyModel.SortBy.Username
isFirstRowAddress: { isFirstRowAddress: {

View File

@ -21,6 +21,7 @@ Control {
property string tokenName property string tokenName
property bool showRemotelyDestructMenuItem: true property bool showRemotelyDestructMenuItem: true
property alias isAirdropEnabled: infoBoxPanel.buttonEnabled property alias isAirdropEnabled: infoBoxPanel.buttonEnabled
property int multiplierIndex: 0
readonly property alias sortBy: holdersList.sortBy readonly property alias sortBy: holdersList.sortBy
readonly property alias sorting: holdersList.sortOrder readonly property alias sorting: holdersList.sortOrder
@ -121,6 +122,7 @@ Control {
id: holdersList id: holdersList
visible: !root.empty && proxyModel.count > 0 visible: !root.empty && proxyModel.count > 0
multiplierIndex: root.multiplierIndex
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: contentHeight Layout.preferredHeight: contentHeight

View File

@ -49,6 +49,7 @@ StatusScrollView {
readonly property bool transferable: token.transferable readonly property bool transferable: token.transferable
readonly property string chainIcon: token.chainIcon readonly property string chainIcon: token.chainIcon
readonly property int decimals: token.decimals readonly property int decimals: token.decimals
readonly property int multiplierIndex: token.multiplierIndex
readonly property bool deploymentCompleted: readonly property bool deploymentCompleted:
deployState === Constants.ContractTransactionStatus.Completed deployState === Constants.ContractTransactionStatus.Completed
@ -203,6 +204,7 @@ StatusScrollView {
showRemotelyDestructMenuItem: !root.isAssetView && root.remotelyDestruct showRemotelyDestructMenuItem: !root.isAssetView && root.remotelyDestruct
isAirdropEnabled: root.deploymentCompleted && isAirdropEnabled: root.deploymentCompleted &&
(token.infiniteSupply || token.remainingTokens > 0) (token.infiniteSupply || token.remainingTokens > 0)
multiplierIndex: root.multiplierIndex
Layout.topMargin: Style.current.padding Layout.topMargin: Style.current.padding
Layout.fillWidth: true Layout.fillWidth: true