From 2087616b824fd46118ea9f937587b5c29b42fdf7 Mon Sep 17 00:00:00 2001 From: Michal Iskierko Date: Thu, 13 Apr 2023 10:09:06 +0200 Subject: [PATCH] feat(@desktop/communities): Adding token owners model - replace qml owners model with Nim one - get token owners from wallet service - keeping owners cache in community_tokens/service and refresh every 10 minutes Issue #10254 --- .../modules/main/communities/controller.nim | 4 +- src/app/modules/main/communities/module.nim | 14 +--- .../communities/tokens/models/token_item.nim | 25 ++++-- .../communities/tokens/models/token_model.nim | 24 +++++- .../tokens/models/token_owners_item.nim | 28 +++++++ .../tokens/models/token_owners_model.nim | 73 ++++++++++++++++++ src/app/modules/main/controller.nim | 9 ++- src/app/modules/main/io_interface.nim | 5 +- src/app/modules/main/module.nim | 28 +++---- .../modules/shared_models/section_item.nim | 8 +- .../service/community_tokens/async_tasks.nim | 28 +++++++ .../dto/community_token_owner.nim | 13 ++++ .../service/community_tokens/service.nim | 76 ++++++++++++++++++- src/backend/community_tokens.nim | 4 + .../CommunityMintTokensSettingsPanel.qml | 22 ++++-- .../panels/communities/TokenHoldersPanel.qml | 10 +-- .../community/RemoteSelfDestructPopup.qml | 7 +- .../Chat/views/CommunitySettingsView.qml | 3 +- .../communities/CommunityCollectibleView.qml | 8 +- .../shared/stores/CommunityTokensStore.qml | 49 +----------- 20 files changed, 327 insertions(+), 111 deletions(-) create mode 100644 src/app/modules/main/communities/tokens/models/token_owners_item.nim create mode 100644 src/app/modules/main/communities/tokens/models/token_owners_model.nim create mode 100644 src/app_service/service/community_tokens/dto/community_token_owner.nim diff --git a/src/app/modules/main/communities/controller.nim b/src/app/modules/main/communities/controller.nim index 9e86d12e54..4d2ce7fd84 100644 --- a/src/app/modules/main/communities/controller.nim +++ b/src/app/modules/main/communities/controller.nim @@ -247,5 +247,5 @@ proc requestCancelDiscordCommunityImport*(self: Controller, id: string) = proc getCommunityTokens*(self: Controller, communityId: string): seq[CommunityTokenDto] = self.communityTokensService.getCommunityTokens(communityId) -proc getNetworks*(self:Controller): seq[NetworkDto] = - self.networksService.getNetworks() +proc getNetwork*(self:Controller, chainId: int): NetworkDto = + self.networksService.getNetwork(chainId) diff --git a/src/app/modules/main/communities/module.nim b/src/app/modules/main/communities/module.nim index 3e4b1a0ed5..a9b10e0e6b 100644 --- a/src/app/modules/main/communities/module.nim +++ b/src/app/modules/main/communities/module.nim @@ -23,7 +23,6 @@ import ../../../../app_service/service/network/service as networks_service import ../../../../app_service/service/transaction/service as transaction_service import ../../../../app_service/service/community_tokens/service as community_tokens_service import ../../../../app_service/service/chat/dto/chat -import ./tokens/models/token_item import ./tokens/module as community_tokens_module export io_interface @@ -132,16 +131,6 @@ proc createMemberItem(self: Module, memberId, requestId: string): MemberItem = isVerified = contactDetails.details.isContactVerified(), requestToJoinId = requestId) -proc createTokenItem(self: Module, tokenDto: CommunityTokenDto) : TokenItem = - var chainName, chainIcon: string - let networks = self.controller.getNetworks() - for network in networks: - if network.chainId == tokenDto.chainId: - chainName = network.chainName - chainIcon = network.iconURL - break - result = initTokenItem(tokenDto, chainName, chainIcon) - method getCommunityItem(self: Module, c: CommunityDto): SectionItem = return initItem( c.id, @@ -179,8 +168,7 @@ method getCommunityItem(self: Module, c: CommunityDto): SectionItem = declinedMemberRequests = c.declinedRequestsToJoin.map(proc(requestDto: CommunityMembershipRequestDto): MemberItem = result = self.createMemberItem(requestDto.publicKey, requestDto.id)), encrypted = c.encrypted, - communityTokens = self.controller.getCommunityTokens(c.id).map(proc(tokenDto: CommunityTokenDto): TokenItem = - result = self.createTokenItem(tokenDto)) + communityTokens = @[] ) proc getCuratedCommunityItem(self: Module, c: CommunityDto): CuratedCommunityItem = diff --git a/src/app/modules/main/communities/tokens/models/token_item.nim b/src/app/modules/main/communities/tokens/models/token_item.nim index 60c18191d5..093c7499fa 100644 --- a/src/app/modules/main/communities/tokens/models/token_item.nim +++ b/src/app/modules/main/communities/tokens/models/token_item.nim @@ -1,5 +1,10 @@ -import strformat +import strformat, sequtils import ../../../../../../app_service/service/community_tokens/dto/community_token +import ../../../../../../app_service/service/collectible/dto +import ../../../../../../app_service/service/network/dto + +import token_owners_model +import token_owners_item export community_token @@ -8,20 +13,28 @@ type tokenDto*: CommunityTokenDto chainName*: string chainIcon*: string + tokenOwnersModel*: token_owners_model.TokenOwnersModel proc initTokenItem*( tokenDto: CommunityTokenDto, - chainName: string, - chainIcon: string, + network: NetworkDto, + tokenOwners: seq[CollectibleOwner] ): TokenItem = result.tokenDto = tokenDto - result.chainName = chainName - result.chainIcon = chainIcon + if network != nil: + result.chainName = network.chainName + result.chainIcon = network.iconURL + result.tokenOwnersModel = newTokenOwnersModel() + result.tokenOwnersModel.setItems(tokenOwners.map(proc(owner: CollectibleOwner): TokenOwnersItem = + # TODO find member with the address - later when airdrop to member will be added + result = initTokenOwnersItem("", "", owner) + )) proc `$`*(self: TokenItem): string = result = fmt"""TokenItem( tokenDto: {self.tokenDto}, chainName: {self.chainName}, - chainIcon: {self.chainIcon} + chainIcon: {self.chainIcon}, + tokenOwnersModel: {self.tokenOwnersModel} ]""" 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 9fdcfbc028..d4f4ab1ec4 100644 --- a/src/app/modules/main/communities/tokens/models/token_model.nim +++ b/src/app/modules/main/communities/tokens/models/token_model.nim @@ -1,6 +1,9 @@ -import NimQml, Tables, strformat +import NimQml, Tables, strformat, sequtils import token_item +import token_owners_item +import token_owners_model import ../../../../../../app_service/service/community_tokens/dto/community_token +import ../../../../../../app_service/service/collectible/dto type ModelRole {.pure.} = enum @@ -18,6 +21,7 @@ type Image ChainName ChainIcon + TokenOwnersModel QtObject: type TokenModel* = ref object of QAbstractListModel @@ -34,14 +38,25 @@ QtObject: new(result, delete) result.setup - proc updateDeployState*(self: TokenModel, contractAddress: string, deployState: DeployState) = + proc updateDeployState*(self: TokenModel, chainId: int, contractAddress: string, deployState: DeployState) = for i in 0 ..< self.items.len: - if(self.items[i].tokenDto.address == contractAddress): + if((self.items[i].tokenDto.address == contractAddress) and (self.items[i].tokenDto.chainId == chainId)): self.items[i].tokenDto.deployState = deployState let index = self.createIndex(i, 0, nil) self.dataChanged(index, index, @[ModelRole.DeployState.int]) return + proc setCommunityTokenOwners*(self: TokenModel, chainId: int, contractAddress: string, owners: seq[CollectibleOwner]) = + for i in 0 ..< self.items.len: + if((self.items[i].tokenDto.address == contractAddress) and (self.items[i].tokenDto.chainId == chainId)): + self.items[i].tokenOwnersModel.setItems(owners.map(proc(owner: CollectibleOwner): TokenOwnersItem = + # TODO find member with the address - later when airdrop to member will be added + result = initTokenOwnersItem("", "", owner) + )) + let index = self.createIndex(i, 0, nil) + self.dataChanged(index, index, @[ModelRole.TokenOwnersModel.int]) + return + proc countChanged(self: TokenModel) {.signal.} proc setItems*(self: TokenModel, items: seq[TokenItem]) = @@ -85,6 +100,7 @@ QtObject: ModelRole.Image.int:"image", ModelRole.ChainName.int:"chainName", ModelRole.ChainIcon.int:"chainIcon", + ModelRole.TokenOwnersModel.int:"tokenOwnersModel", }.toTable method data(self: TokenModel, index: QModelIndex, role: int): QVariant = @@ -123,6 +139,8 @@ QtObject: result = newQVariant(item.chainName) of ModelRole.ChainIcon: result = newQVariant(item.chainIcon) + of ModelRole.TokenOwnersModel: + result = newQVariant(item.tokenOwnersModel) proc `$`*(self: TokenModel): string = for i in 0 ..< self.items.len: 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 new file mode 100644 index 0000000000..5c782713ae --- /dev/null +++ b/src/app/modules/main/communities/tokens/models/token_owners_item.nim @@ -0,0 +1,28 @@ +import strformat, stint +import ../../../../../../app_service/service/collectible/dto + +type + TokenOwnersItem* = object + name*: string + imageSource*: string + ownerDetails*: CollectibleOwner + amount*: int + +proc initTokenOwnersItem*( + name: string, + imageSource: string, + ownerDetails: CollectibleOwner +): TokenOwnersItem = + result.name = name + result.imageSource = imageSource + result.ownerDetails = ownerDetails + for balance in ownerDetails.balances: + result.amount = result.amount + balance.balance.truncate(int) + +proc `$`*(self: TokenOwnersItem): string = + result = fmt"""TokenOwnersItem( + name: {self.name}, + amount: {self.amount}, + ownerDetails: {self.ownerDetails} + ]""" + 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 new file mode 100644 index 0000000000..af99225019 --- /dev/null +++ b/src/app/modules/main/communities/tokens/models/token_owners_model.nim @@ -0,0 +1,73 @@ +import NimQml, Tables, strformat +import token_owners_item + +type + ModelRole {.pure.} = enum + Name = UserRole + 1 + ImageSource + WalletAddress + Amount + +QtObject: + type TokenOwnersModel* = ref object of QAbstractListModel + items*: seq[TokenOwnersItem] + + proc setup(self: TokenOwnersModel) = + self.QAbstractListModel.setup + + proc delete(self: TokenOwnersModel) = + self.items = @[] + self.QAbstractListModel.delete + + proc newTokenOwnersModel*(): TokenOwnersModel = + new(result, delete) + result.setup + + proc countChanged(self: TokenOwnersModel) {.signal.} + + proc setItems*(self: TokenOwnersModel, items: seq[TokenOwnersItem]) = + self.beginResetModel() + self.items = items + self.endResetModel() + self.countChanged() + + proc count*(self: TokenOwnersModel): int {.slot.} = + self.items.len + + QtProperty[int] count: + read = count + notify = countChanged + + method rowCount(self: TokenOwnersModel, index: QModelIndex = nil): int = + return self.items.len + + method roleNames(self: TokenOwnersModel): Table[int, string] = + { + ModelRole.Name.int:"name", + ModelRole.ImageSource.int:"imageSource", + ModelRole.WalletAddress.int:"walletAddress", + ModelRole.Amount.int:"amount", + }.toTable + + method data(self: TokenOwnersModel, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.items.len: + return + let item = self.items[index.row] + let enumRole = role.ModelRole + case enumRole: + of ModelRole.Name: + result = newQVariant(item.name) + of ModelRole.ImageSource: + result = newQVariant(item.imageSource) + of ModelRole.WalletAddress: + result = newQVariant(item.ownerDetails.address) + of ModelRole.Amount: + result = newQVariant(item.amount) + + proc `$`*(self: TokenOwnersModel): string = + for i in 0 ..< self.items.len: + result &= fmt"""TokenOwnersModel: + [{i}]:({$self.items[i]}) + """ \ No newline at end of file diff --git a/src/app/modules/main/controller.nim b/src/app/modules/main/controller.nim index e603eec070..2df3fe607d 100644 --- a/src/app/modules/main/controller.nim +++ b/src/app/modules/main/controller.nim @@ -341,7 +341,11 @@ proc init*(self: Controller) = self.events.on(SIGNAL_COMMUNITY_TOKEN_DEPLOY_STATUS) do(e: Args): let args = CommunityTokenDeployedStatusArgs(e) - self.delegate.onCommunityTokenDeployStateChanged(args.communityId, args.contractAddress, args.deployState) + self.delegate.onCommunityTokenDeployStateChanged(args.communityId, args.chainId, args.contractAddress, args.deployState) + + self.events.on(SIGNAL_COMMUNITY_TOKEN_OWNERS_FETCHED) do(e: Args): + let args = CommunityTokenOwnersArgs(e) + self.delegate.onCommunityTokenOwnersFetched(args.communityId, args.chainId, args.contractAddress, args.owners) self.events.on(SIGNAL_ACCEPT_REQUEST_TO_JOIN_LOADING) do(e: Args): var args = CommunityMemberArgs(e) @@ -467,5 +471,8 @@ proc getVerificationRequestFrom*(self: Controller, publicKey: string): Verificat proc getCommunityTokens*(self: Controller, communityId: string): seq[CommunityTokenDto] = self.communityTokensService.getCommunityTokens(communityId) +proc getCommunityTokenOwners*(self: Controller, communityId: string, chainId: int, contractAddress: string): seq[CollectibleOwner] = + return self.communityTokensService.getCommunityTokenOwners(communityId, chainId, contractAddress) + proc getNetwork*(self:Controller, chainId: int): NetworkDto = self.networksService.getNetwork(chainId) \ No newline at end of file diff --git a/src/app/modules/main/io_interface.nim b/src/app/modules/main/io_interface.nim index a9955f94aa..4dcc878a5a 100644 --- a/src/app/modules/main/io_interface.nim +++ b/src/app/modules/main/io_interface.nim @@ -297,7 +297,10 @@ method onSharedKeycarModuleKeycardSyncPurposeTerminated*(self: AccessInterface, method onCommunityTokenDeployed*(self: AccessInterface, communityToken: CommunityTokenDto) {.base.} = raise newException(ValueError, "No implementation available") -method onCommunityTokenDeployStateChanged*(self: AccessInterface, communityId: string, contractAddress: string, deployState: DeployState) {.base.} = +method onCommunityTokenOwnersFetched*(self: AccessInterface, communityId: string, chainId: int, contractAddress: string, owners: seq[CollectibleOwner]) {.base.} = + raise newException(ValueError, "No implementation available") + +method onCommunityTokenDeployStateChanged*(self: AccessInterface, communityId: string, chainId: int, contractAddress: string, deployState: DeployState) {.base.} = raise newException(ValueError, "No implementation available") method onAcceptRequestToJoinFailed*(self: AccessInterface, communityId: string, memberKey: string, requestId: string) {.base.} = diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index 5effc2fa1f..b2e3bb36ab 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -237,12 +237,10 @@ method delete*[T](self: Module[T]) = self.view.delete self.viewVariant.delete -proc createTokenItem[T](self: Module[T], tokenDto: CommunityTokenDto, network: NetworkDto) : TokenItem = - var chainName, chainIcon: string - if network != nil: - chainName = network.chainName - chainIcon = network.iconURL - result = initTokenItem(tokenDto, chainName, chainIcon) +proc createTokenItem[T](self: Module[T], tokenDto: CommunityTokenDto) : TokenItem = + let network = self.controller.getNetwork(tokenDto.chainId) + let tokenOwners = self.controller.getCommunityTokenOwners(tokenDto.communityId, tokenDto.chainId, tokenDto.address) + result = initTokenItem(tokenDto, network, tokenOwners) proc createChannelGroupItem[T](self: Module[T], channelGroup: ChannelGroupDto): SectionItem = let isCommunity = channelGroup.channelGroupType == ChannelGroupType.Community @@ -252,8 +250,7 @@ proc createChannelGroupItem[T](self: Module[T], channelGroup: ChannelGroupDto): communityDetails = self.controller.getCommunityById(channelGroup.id) let communityTokens = self.controller.getCommunityTokens(channelGroup.id) communityTokensItems = communityTokens.map(proc(tokenDto: CommunityTokenDto): TokenItem = - let network = self.controller.getNetwork(tokenDto.chainId) - result = self.createTokenItem(tokenDto, network) + result = self.createTokenItem(tokenDto) ) let unviewedCount = channelGroup.unviewedMessagesCount @@ -1004,16 +1001,20 @@ method contactsStatusUpdated*[T](self: Module[T], statusUpdates: seq[StatusUpdat let status = toOnlineStatus(s.statusType) self.view.activeSection().setOnlineStatusForMember(s.publicKey, status) -method onCommunityTokenDeployed*[T](self: Module[T], communityToken: CommunityTokenDto) {.base.} = +method onCommunityTokenDeployed*[T](self: Module[T], communityToken: CommunityTokenDto) = let item = self.view.model().getItemById(communityToken.communityId) if item.id != "": - let network = self.controller.getNetwork(communityToken.chainId) - item.appendCommunityToken(self.createTokenItem(communityToken, network)) + item.appendCommunityToken(self.createTokenItem(communityToken)) -method onCommunityTokenDeployStateChanged*[T](self: Module[T], communityId: string, contractAddress: string, deployState: DeployState) = +method onCommunityTokenOwnersFetched*[T](self: Module[T], communityId: string, chainId: int, contractAddress: string, owners: seq[CollectibleOwner]) = let item = self.view.model().getItemById(communityId) if item.id != "": - item.updateCommunityTokenDeployState(contractAddress, deployState) + item.setCommunityTokenOwners(chainId, contractAddress, owners) + +method onCommunityTokenDeployStateChanged*[T](self: Module[T], communityId: string, chainId: int, contractAddress: string, deployState: DeployState) = + let item = self.view.model().getItemById(communityId) + if item.id != "": + item.updateCommunityTokenDeployState(chainId, contractAddress, deployState) method onAcceptRequestToJoinLoading*[T](self: Module[T], communityId: string, memberKey: string) = let item = self.view.model().getItemById(communityId) @@ -1220,3 +1221,4 @@ method activateStatusDeepLink*[T](self: Module[T], statusDeepLink: string) = method onDeactivateChatLoader*[T](self: Module[T], sectionId: string, chatId: string) = if (sectionId.len > 0 and self.channelGroupModules.contains(sectionId)): self.channelGroupModules[sectionId].onDeactivateChatLoader(chatId) + \ No newline at end of file diff --git a/src/app/modules/shared_models/section_item.nim b/src/app/modules/shared_models/section_item.nim index cdf4f6ac73..3956e627c1 100644 --- a/src/app/modules/shared_models/section_item.nim +++ b/src/app/modules/shared_models/section_item.nim @@ -3,6 +3,7 @@ import ./member_model, ./member_item import ../main/communities/models/[pending_request_item, pending_request_model] import ../main/communities/tokens/models/token_model as community_tokens_model import ../main/communities/tokens/models/token_item +import ../../../app_service/service/collectible/dto import ../../global/global_singleton @@ -321,8 +322,11 @@ proc encrypted*(self: SectionItem): bool {.inline.} = proc appendCommunityToken*(self: SectionItem, item: TokenItem) {.inline.} = self.communityTokensModel.appendItem(item) -proc updateCommunityTokenDeployState*(self: SectionItem, contractAddress: string, deployState: DeployState) {.inline.} = - self.communityTokensModel.updateDeployState(contractAddress, deployState) +proc updateCommunityTokenDeployState*(self: SectionItem, chainId: int, contractAddress: string, deployState: DeployState) {.inline.} = + self.communityTokensModel.updateDeployState(chainId, contractAddress, deployState) + +proc setCommunityTokenOwners*(self: SectionItem, chainId: int, contractAddress: string, owners: seq[CollectibleOwner]) {.inline.} = + self.communityTokensModel.setCommunityTokenOwners(chainId, contractAddress, owners) proc communityTokens*(self: SectionItem): community_tokens_model.TokenModel {.inline.} = self.communityTokensModel diff --git a/src/app_service/service/community_tokens/async_tasks.nim b/src/app_service/service/community_tokens/async_tasks.nim index 0b3d60b31b..ebd1a18e4f 100644 --- a/src/app_service/service/community_tokens/async_tasks.nim +++ b/src/app_service/service/community_tokens/async_tasks.nim @@ -1,5 +1,6 @@ include ../../common/json_utils import ../../../backend/eth +import ../../../backend/collectibles import ../../../app/core/tasks/common import ../../../app/core/tasks/qt import ../transaction/dto @@ -21,3 +22,30 @@ const asyncGetSuggestedFeesTask: Task = proc(argEncoded: string) {.gcsafe, nimca "error": e.msg, }) +type + FetchCollectibleOwnersArg = ref object of QObjectTaskArg + chainId*: int + contractAddress*: string + communityId*: string + +const fetchCollectibleOwnersTaskArg: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[FetchCollectibleOwnersArg](argEncoded) + try: + let response = collectibles.getCollectibleOwnersByContractAddress(arg.chainId, arg.contractAddress) + let output = %* { + "chainId": arg.chainId, + "contractAddress": arg.contractAddress, + "communityId": arg.communityId, + "result": response.result, + "error": "" + } + arg.finish(output) + except Exception as e: + let output = %* { + "chainId": arg.chainId, + "contractAddress": arg.contractAddress, + "communityId": arg.communityId, + "result": "", + "error": e.msg + } + arg.finish(output) \ No newline at end of file diff --git a/src/app_service/service/community_tokens/dto/community_token_owner.nim b/src/app_service/service/community_tokens/dto/community_token_owner.nim new file mode 100644 index 0000000000..ded225a4b7 --- /dev/null +++ b/src/app_service/service/community_tokens/dto/community_token_owner.nim @@ -0,0 +1,13 @@ +import json + +type + CommunityTokenOwner* = object + walletAddress*: string + amount*: int + +proc `%`*(x: CommunityTokenOwner): JsonNode = + result = newJobject() + result["walletAddress"] = %x.walletAddress + result["amount"] = %x.amount + + diff --git a/src/app_service/service/community_tokens/service.nim b/src/app_service/service/community_tokens/service.nim index d61f7a3e3e..fe32695110 100644 --- a/src/app_service/service/community_tokens/service.nim +++ b/src/app_service/service/community_tokens/service.nim @@ -10,6 +10,7 @@ import ../settings/service as settings_service import ../wallet_account/service as wallet_account_service import ../ens/utils as ens_utils import ../eth/dto/transaction +import ../collectible/dto as collectibles_dto import ../../../backend/response_type @@ -18,12 +19,15 @@ import ../community/dto/community import ./dto/deployment_parameters import ./dto/community_token -import ./airdrop_details +import ./dto/community_token_owner + +import airdrop_details include async_tasks export community_token export deployment_parameters +export community_token_owner logScope: topics = "community-tokens-service" @@ -59,14 +63,23 @@ type fiatCurrency*: CurrencyAmount errorCode*: ComputeFeeErrorCode -type ContractTuple = tuple +type + ContractTuple = tuple address: string chainId: int +type + CommunityTokenOwnersArgs* = ref object of Args + communityId*: string + contractAddress*: string + chainId*: int + owners*: seq[CollectibleOwner] + # Signals which may be emitted by this service: const SIGNAL_COMMUNITY_TOKEN_DEPLOY_STATUS* = "communityTokenDeployStatus" const SIGNAL_COMMUNITY_TOKEN_DEPLOYED* = "communityTokenDeployed" const SIGNAL_COMPUTE_DEPLOY_FEE* = "computeDeployFee" +const SIGNAL_COMMUNITY_TOKEN_OWNERS_FETCHED* = "communityTokenOwnersFetched" QtObject: type @@ -80,8 +93,14 @@ QtObject: tempAccountAddress: string tempChainId: int addressAndTxMap: Table[ContractTuple, string] + tokenOwnersTimer: QTimer + tokenOwnersCache: Table[ContractTuple, seq[CollectibleOwner]] + + # Forward declaration + proc fetchAllTokenOwners*(self: Service) proc delete*(self: Service) = + delete(self.tokenOwnersTimer) self.QObject.delete proc newService*( @@ -101,8 +120,13 @@ QtObject: result.settingsService = settingsService result.walletAccountService = walletAccountService result.addressAndTxMap = initTable[ContractTuple, string]() + result.tokenOwnersTimer = newQTimer() + result.tokenOwnersTimer.setInterval(10*60*1000) + signalConnect(result.tokenOwnersTimer, "timeout()", result, "onRefreshTransferableTokenOwners()", 2) proc init*(self: Service) = + self.fetchAllTokenOwners() + self.tokenOwnersTimer.start() self.events.on(PendingTransactionTypeDto.CollectibleDeployment.event) do(e: Args): var receivedData = TransactionMinedArgs(e) let deployState = if receivedData.success: DeployState.Deployed else: DeployState.Failed @@ -140,7 +164,6 @@ QtObject: proc deployCollectibles*(self: Service, communityId: string, addressFrom: string, password: string, deploymentParams: DeploymentParameters, tokenMetadata: CommunityTokensMetadataDto, chainId: int) = try: - # TODO this will come from SendModal let suggestedFees = self.transactionService.suggestedFees(chainId) let contractGasUnits = self.deployCollectiblesEstimate() if suggestedFees == nil: @@ -202,6 +225,13 @@ QtObject: except RpcException: error "Error getting community tokens", message = getCurrentExceptionMsg() + proc getAllCommunityTokens*(self: Service): seq[CommunityTokenDto] = + try: + let response = tokens_backend.getAllCommunityTokens() + return parseCommunityTokens(response) + except RpcException: + error "Error getting all community tokens", message = getCurrentExceptionMsg() + proc getCommunityTokenBySymbol*(self: Service, communityId: string, symbol: string): CommunityTokenDto = let communityTokens = self.getCommunityTokens(communityId) for token in communityTokens: @@ -303,3 +333,43 @@ QtObject: let data = ComputeDeployFeeArgs(ethCurrency: ethCurrency, fiatCurrency: fiatCurrency, errorCode: (if ethValue > balance: ComputeFeeErrorCode.Balance else: ComputeFeeErrorCode.Success)) self.events.emit(SIGNAL_COMPUTE_DEPLOY_FEE, data) + + proc fetchCommunityOwners*(self: Service, communityId: string, chainId: int, contractAddress: string) = + let arg = FetchCollectibleOwnersArg( + tptr: cast[ByteAddress](fetchCollectibleOwnersTaskArg), + vptr: cast[ByteAddress](self.vptr), + slot: "onCommunityTokenOwnersFetched", + chainId: chainId, + contractAddress: contractAddress, + communityId: communityId + ) + self.threadpool.start(arg) + + # get owners from cache + proc getCommunityTokenOwners*(self: Service, communityId: string, chainId: int, contractAddress: string): seq[CollectibleOwner] = + return self.tokenOwnersCache.getOrDefault((address: contractAddress, chainId: chainId)) + + 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 + return + let chainId = responseJson["chainId"].getInt + let contractAddress = responseJson["contractAddress"].getStr + let communityId = responseJson["communityId"].getStr + let resultJson = responseJson["result"] + let owners = collectibles_dto.toCollectibleOwnershipDto(resultJson).owners + let data = CommunityTokenOwnersArgs(chainId: chainId, contractAddress: contractAddress, communityId: communityId, owners: owners) + self.events.emit(SIGNAL_COMMUNITY_TOKEN_OWNERS_FETCHED, data) + + proc onRefreshTransferableTokenOwners*(self:Service) {.slot.} = + let allTokens = self.getAllCommunityTokens() + for token in allTokens: + if token.transferable: + self.fetchCommunityOwners(token.communityId, token.chainId, token.address) + + proc fetchAllTokenOwners*(self: Service) = + let allTokens = self.getAllCommunityTokens() + for token in allTokens: + self.fetchCommunityOwners(token.communityId, token.chainId, token.address) \ No newline at end of file diff --git a/src/backend/community_tokens.nim b/src/backend/community_tokens.nim index e4ad8eab83..c78ccc646b 100644 --- a/src/backend/community_tokens.nim +++ b/src/backend/community_tokens.nim @@ -12,6 +12,10 @@ proc getCommunityTokens*(communityId: string): RpcResponse[JsonNode] {.raises: [ let payload = %* [communityId] return core.callPrivateRPC("wakuext_getCommunityTokens", payload) +proc getAllCommunityTokens*(): RpcResponse[JsonNode] {.raises: [Exception].} = + let payload = %* [] + return core.callPrivateRPC("wakuext_getAllCommunityTokens", payload) + proc addCommunityToken*(token: CommunityTokenDto): RpcResponse[JsonNode] {.raises: [Exception].} = let payload = %* [token.toJsonNode()] return core.callPrivateRPC("wakuext_addCommunityToken", payload) diff --git a/ui/app/AppLayouts/Chat/panels/communities/CommunityMintTokensSettingsPanel.qml b/ui/app/AppLayouts/Chat/panels/communities/CommunityMintTokensSettingsPanel.qml index 13836bf81b..440d8c47a4 100644 --- a/ui/app/AppLayouts/Chat/panels/communities/CommunityMintTokensSettingsPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/communities/CommunityMintTokensSettingsPanel.qml @@ -20,7 +20,7 @@ SettingsPageLayout { // Models: property var tokensModel - property var holdersModel + property string feeText property string errorText property bool isFeeLoading: true @@ -51,7 +51,7 @@ SettingsPageLayout { signal signMintTransactionOpened(int chainId, string accountAddress) - signal remoteSelfDestructCollectibles(var holdersModel, + signal remoteSelfDestructCollectibles(var tokenOwnersModel, int chainId, string accountName, string accountAddress) @@ -89,6 +89,8 @@ SettingsPageLayout { property int chainId property string chainName + property var tokenOwnersModel + readonly property var initialItem: (root.tokensModel && root.tokensModel.count > 0) ? mintedTokensView : welcomeView onInitialItemChanged: updateInitialStackView() @@ -228,7 +230,6 @@ SettingsPageLayout { } viewWidth: root.viewWidth - holdersModel: root.holdersModel onMintCollectible: popup.open() @@ -283,7 +284,7 @@ SettingsPageLayout { id: remoteSelfdestructPopup collectibleName: root.title - model: root.holdersModel + model: d.tokenOwnersModel onSelfDestructClicked: { alertPopup.tokenCount = tokenCount @@ -303,7 +304,7 @@ SettingsPageLayout { function signSelfRemoteDestructTransaction() { root.isFeeLoading = true root.feeText = "" - root.remoteSelfDestructCollectibles(root.holdersModel, + root.remoteSelfDestructCollectibles(d.tokenOwnersModel, d.chainId, d.accountName, d.accountAddress) @@ -356,7 +357,6 @@ SettingsPageLayout { property int index // TODO: Update it to key when model has role key implemented viewWidth: root.viewWidth - holdersModel: root.holdersModel Binding { target: root @@ -364,9 +364,16 @@ SettingsPageLayout { value: view.name } + Binding { + target: d + property: "tokenOwnersModel" + value: view.tokenOwnersModel + } + Instantiator { id: instantiator + model: SortFilterProxyModel { sourceModel: root.tokensModel filters: IndexFilter { @@ -388,7 +395,8 @@ SettingsPageLayout { Bind { property: "chainId"; value: model.chainId }, Bind { property: "chainName"; value: model.chainName }, Bind { property: "chainIcon"; value: model.chainIcon }, - Bind { property: "accountName"; value: model.accountName } + Bind { property: "accountName"; value: model.accountName }, + Bind { property: "tokenOwnersModel"; value: model.tokenOwnersModel } ] } } diff --git a/ui/app/AppLayouts/Chat/panels/communities/TokenHoldersPanel.qml b/ui/app/AppLayouts/Chat/panels/communities/TokenHoldersPanel.qml index 13b94e7aac..3c928fc186 100644 --- a/ui/app/AppLayouts/Chat/panels/communities/TokenHoldersPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/communities/TokenHoldersPanel.qml @@ -15,7 +15,7 @@ import shared.controls 1.0 Control { id: root - // Expected roles: ensName, walletAddress, imageSource, amount, selfDestructAmount and selfDestruct + // Expected roles: name, walletAddress, imageSource, amount, selfDestructAmount and selfDestruct property var model property string tokenName @@ -42,7 +42,7 @@ Control { enabled: searcher.enabled expression: { searcher.text - return model.ensName.toLowerCase().includes(searcher.text.toLowerCase()) || + return model.name.toLowerCase().includes(searcher.text.toLowerCase()) || model.walletAddress.toLowerCase().includes(searcher.text.toLowerCase()) } } @@ -57,7 +57,7 @@ Control { bottomPadding: 0 minimumHeight: 36 // by design maximumHeight: minimumHeight - enabled: root.model.count > 0 + enabled: root.model && root.model.count > 0 placeholderText: enabled ? qsTr("Search") : qsTr("No placeholders to search") } @@ -81,8 +81,8 @@ Control { spacing: Style.current.padding StatusListItem { - readonly property bool unknownHolder: model.ensName === "" - readonly property string formattedTitle: unknownHolder ? "?" : model.ensName + readonly property bool unknownHolder: model.name === "" + readonly property string formattedTitle: unknownHolder ? "?" : model.name Layout.fillWidth: true diff --git a/ui/app/AppLayouts/Chat/popups/community/RemoteSelfDestructPopup.qml b/ui/app/AppLayouts/Chat/popups/community/RemoteSelfDestructPopup.qml index dc34886264..d4d749c538 100644 --- a/ui/app/AppLayouts/Chat/popups/community/RemoteSelfDestructPopup.qml +++ b/ui/app/AppLayouts/Chat/popups/community/RemoteSelfDestructPopup.qml @@ -7,6 +7,7 @@ import StatusQ.Core 0.1 import StatusQ.Controls 0.1 import StatusQ.Popups.Dialog 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 import AppLayouts.Chat.panels.communities 1.0 @@ -36,11 +37,11 @@ StatusDialog { } function calculateTotalTokensToDestruct() { - tokenCount = 0 + d.tokenCount = 0 for(var i = 0; i < tokenHoldersPanel.model.count; i ++) { - var item = tokenHoldersPanel.model.get(i) + var item = ModelUtils.get(tokenHoldersPanel.model, i) if(item.selfDestruct) { - tokenCount += item.selfDestructAmount + d.tokenCount += item.selfDestructAmount } } } diff --git a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml index f029942a93..bb3fab182a 100644 --- a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml +++ b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml @@ -277,7 +277,6 @@ StatusSectionLayout { rootStore.communityTokensStore tokensModel: root.community.communityTokens - holdersModel: communityTokensStore.holdersModel layer1Networks: communityTokensStore.layer1Networks layer2Networks: communityTokensStore.layer2Networks testNetworks: communityTokensStore.testNetworks @@ -303,7 +302,7 @@ StatusSectionLayout { } onSignSelfDestructTransactionOpened: communityTokensStore.computeSelfDestructFee(chainId) onRemoteSelfDestructCollectibles: { - communityTokensStore.remoteSelfDestructCollectibles(holdersModel, + communityTokensStore.remoteSelfDestructCollectibles(tokenOwnersModel, chainId, accountName, accountAddress) diff --git a/ui/app/AppLayouts/Chat/views/communities/CommunityCollectibleView.qml b/ui/app/AppLayouts/Chat/views/communities/CommunityCollectibleView.qml index 7271f0fa67..108c3a6ba9 100644 --- a/ui/app/AppLayouts/Chat/views/communities/CommunityCollectibleView.qml +++ b/ui/app/AppLayouts/Chat/views/communities/CommunityCollectibleView.qml @@ -16,7 +16,6 @@ StatusScrollView { property int viewWidth: 560 // by design property bool preview: false - property var holdersModel // Collectible object properties: property alias artworkSource: image.source @@ -31,6 +30,8 @@ StatusScrollView { property int chainId property string chainIcon property int deployState + property var tokenOwnersModel + property alias accountName: accountBox.value signal mintCollectible(url artworkSource, @@ -248,11 +249,10 @@ StatusScrollView { } } - // Disabled until backend is ready (milestone 12) TokenHoldersPanel { - visible: false//!root.preview + visible: !root.preview tokenName: root.name - model: root.holdersModel + model: root.tokenOwnersModel Layout.topMargin: Style.current.padding Layout.fillWidth: true Layout.fillHeight: true diff --git a/ui/imports/shared/stores/CommunityTokensStore.qml b/ui/imports/shared/stores/CommunityTokensStore.qml index dc26412094..94437b4d40 100644 --- a/ui/imports/shared/stores/CommunityTokensStore.qml +++ b/ui/imports/shared/stores/CommunityTokensStore.qml @@ -13,48 +13,9 @@ QtObject { property var enabledNetworks: networksModule.enabled property var allNetworks: networksModule.all - // Token holders model: MOCKED DATA -> TODO: Update with real data - readonly property var holdersModel: ListModel { - - readonly property string image: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAlklEQVR4nOzW0QmDQBAG4SSkl7SUQlJGCrElq9F3QdjjVhh/5nv3cFhY9vUIYQiNITSG0BhCExPynn1gWf9bx498P7/ - nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2ImYgiNITTlTdG1nUZ5a92VITQxITFiJmIIjSE0htAYQrMHAAD//+wwFVpz+yqXAAAAAElFTkSuQmCC" - - Component.onCompleted: - append([ - { - ensName: "carmen.eth", - walletAddress: "0xb794f5450ba39494ce839613fffba74279579268", - imageSource:image, - amount: 3, - selfDestructAmount: 0, - selfDestruct: false - }, - { - ensName: "chris.eth", - walletAddress: "0xb794f5ea0ba39494ce839613fffba74279579268", - imageSource: image, - amount: 2, - selfDestructAmount: 0, - selfDestruct: false - }, - { - ensName: "emily.eth", - walletAddress: "0xb794f5ea0ba39494ce839613fffba74279579268", - imageSource: image, - amount: 2, - selfDestructAmount: 0, - selfDestruct: false - }, - { - ensName: "", - walletAddress: "0xb794f5ea0ba39494ce839613fffba74279579268", - imageSource: "", - amount: 1, - selfDestructAmount: 0, - selfDestruct: false - } - ]) - } + signal deployFeeUpdated(var ethCurrency, var fiatCurrency, int error) + signal deploymentStateChanged(string communityId, int status, string url) + signal selfDestructFeeUpdated(string value) // TO BE REMOVED // Minting tokens: function deployCollectible(communityId, accountAddress, name, symbol, description, supply, @@ -65,10 +26,6 @@ QtObject { infiniteSupply, transferable, selfDestruct, chainId, artworkSource) } - signal deployFeeUpdated(var ethCurrency, var fiatCurrency, int error) - signal deploymentStateChanged(string communityId, int status, string url) - signal selfDestructFeeUpdated(string value) // TO BE REMOVED - readonly property Connections connections: Connections { target: communityTokensModuleInst function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode) {