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:
parent
9410de4286
commit
f00493ec02
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1230,10 +1230,18 @@ 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),
|
||||
|
@ -1243,48 +1251,26 @@ QtObject:
|
|||
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))
|
||||
return
|
||||
|
||||
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:
|
||||
|
|
|
@ -322,3 +322,8 @@ rpc(fetchAllCurrencyFormats, "wallet"):
|
|||
|
||||
rpc(hasPairedDevices, "accounts"):
|
||||
discard
|
||||
|
||||
rpc(getBalancesByChain, "wallet"):
|
||||
chainIds: seq[int]
|
||||
addresses: seq[string]
|
||||
tokenAddresses: seq[string]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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 {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue