fix(@desktop/communities): Lazy loading token holders

Token holders are not fetched when application starts.
They are fetched only when token details screen is opened.

Fix #14974
This commit is contained in:
Michal Iskierko 2024-06-03 10:31:26 +02:00 committed by Michał Iskierko
parent 146a6e8501
commit a7b9a62745
9 changed files with 136 additions and 55 deletions

View File

@ -178,3 +178,9 @@ proc declineOwnership*(self: Controller, communityId: string) =
proc asyncGetOwnerTokenOwnerAddress*(self: Controller, chainId: int, contractAddress: string) = proc asyncGetOwnerTokenOwnerAddress*(self: Controller, chainId: int, contractAddress: string) =
self.communityTokensService.asyncGetOwnerTokenOwnerAddress(chainId, contractAddress) self.communityTokensService.asyncGetOwnerTokenOwnerAddress(chainId, contractAddress)
proc startTokenHoldersManagement*(self: Controller, chainId: int, contractAddress: string) =
self.communityTokensService.startTokenHoldersManagement(chainId, contractAddress)
proc stopTokenHoldersManagement*(self: Controller) =
self.communityTokensService.stopTokenHoldersManagement()

View File

@ -118,4 +118,10 @@ method onOwnerTokenOwnerAddress*(self: AccessInterface, chainId: int, contractAd
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method asyncGetOwnerTokenDetails*(self: AccessInterface, communityId: string) {.base.} = method asyncGetOwnerTokenDetails*(self: AccessInterface, communityId: string) {.base.} =
raise newException(ValueError, "No implementation available")
method startTokenHoldersManagement*(self: AccessInterface, chainId: int, contractAddress: string) {.base.} =
raise newException(ValueError, "No implementation available")
method stopTokenHoldersManagement*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")

View File

@ -385,3 +385,9 @@ method onOwnerTokenOwnerAddress*(self: Module, chainId: int, contractAddress: st
"contractAddress": contractAddress "contractAddress": contractAddress
} }
self.view.setOwnerTokenDetails($jsonObj) self.view.setOwnerTokenDetails($jsonObj)
method startTokenHoldersManagement*(self: Module, chainId: int, contractAddress: string) =
self.controller.startTokenHoldersManagement(chainId, contractAddress)
method stopTokenHoldersManagement*(self: Module) =
self.controller.stopTokenHoldersManagement()

View File

@ -145,3 +145,9 @@ QtObject:
QtProperty[string] ownerTokenDetails: QtProperty[string] ownerTokenDetails:
read = getOwnerTokenDetails read = getOwnerTokenDetails
notify = ownerTokenDetailsChanged notify = ownerTokenDetailsChanged
proc startTokenHoldersManagement*(self: View, chainId: int, contractAddress: string) {.slot.} =
self.communityTokensModule.startTokenHoldersManagement(chainId, contractAddress)
proc stopTokenHoldersManagement*(self: View) {.slot.} =
self.communityTokensModule.stopTokenHoldersManagement()

View File

@ -1,4 +1,4 @@
import NimQml, Tables, chronicles, json, stint, strutils, sugar, sequtils, stew/shims/strformat import NimQml, Tables, chronicles, json, stint, strutils, sugar, sequtils, stew/shims/strformat, times
import ../../../app/global/global_singleton import ../../../app/global/global_singleton
import ../../../app/core/eventemitter import ../../../app/core/eventemitter
import ../../../app/core/tasks/[qt, threadpool] import ../../../app/core/tasks/[qt, threadpool]
@ -287,9 +287,6 @@ QtObject:
communityService: community_service.Service communityService: community_service.Service
currencyService: currency_service.Service currencyService: currency_service.Service
tokenOwnersTimer: QTimer
tokenOwners1SecTimer: QTimer # used to update 1 sec after changes in owners
tempTokenOwnersToFetch: CommunityTokenDto # used by 1sec timer
tokenOwnersCache: Table[ContractTuple, seq[CommunityCollectibleOwner]] tokenOwnersCache: Table[ContractTuple, seq[CommunityCollectibleOwner]]
tempFeeTable: Table[int, SuggestedFeesDto] # fees per chain, filled during gas computation, used during operation (deployment, mint, burn) tempFeeTable: Table[int, SuggestedFeesDto] # fees per chain, filled during gas computation, used during operation (deployment, mint, burn)
@ -298,19 +295,26 @@ QtObject:
communityTokensCache: seq[CommunityTokenDto] communityTokensCache: seq[CommunityTokenDto]
communityDataLoaded: bool # keep times when token holders list for contracts were updated
allCommunityTokensLoaded: bool tokenHoldersLastUpdateMap: Table[ContractTuple, int64]
# timer which fetches token holders
tokenHoldersTimer: QTimer
# token for which token holders are fetched
tokenHoldersToken: CommunityTokenDto
# flag to indicate that token holders management started
tokenHoldersManagementStarted: bool
# Forward declaration # Forward declaration
proc getAllCommunityTokensAsync*(self: Service) proc getAllCommunityTokensAsync*(self: Service)
proc fetchAllTokenOwners(self: Service)
proc getCommunityTokenOwners*(self: Service, communityId: string, chainId: int, contractAddress: string): seq[CommunityCollectibleOwner] proc getCommunityTokenOwners*(self: Service, communityId: string, chainId: int, contractAddress: string): seq[CommunityCollectibleOwner]
proc getCommunityToken*(self: Service, chainId: int, address: string): CommunityTokenDto proc getCommunityToken*(self: Service, chainId: int, address: string): CommunityTokenDto
proc findContractByUniqueId*(self: Service, contractUniqueKey: string): CommunityTokenDto proc findContractByUniqueId*(self: Service, contractUniqueKey: string): CommunityTokenDto
proc restartTokenHoldersTimer(self: Service, chainId: int, contractAddress: string)
proc refreshTokenHolders(self: Service, token: CommunityTokenDto)
proc delete*(self: Service) = proc delete*(self: Service) =
delete(self.tokenOwnersTimer) delete(self.tokenHoldersTimer)
delete(self.tokenOwners1SecTimer)
self.QObject.delete self.QObject.delete
proc newService*( proc newService*(
@ -335,13 +339,10 @@ QtObject:
result.acService = acService result.acService = acService
result.communityService = communityService result.communityService = communityService
result.currencyService = currencyService result.currencyService = currencyService
result.tokenOwnersTimer = newQTimer()
result.tokenOwnersTimer.setInterval(5*60*1000) result.tokenHoldersTimer = newQTimer()
signalConnect(result.tokenOwnersTimer, "timeout()", result, "onRefreshTransferableTokenOwners()", 2) result.tokenHoldersTimer.setSingleShot(true)
result.tokenOwners1SecTimer = newQTimer() signalConnect(result.tokenHoldersTimer, "timeout()", result, "onTokenHoldersTimeout()", 2)
result.tokenOwners1SecTimer.setInterval(1000)
result.tokenOwners1SecTimer.setSingleShot(true)
signalConnect(result.tokenOwners1SecTimer, "timeout()", result, "onFetchTempTokenOwners()", 2)
# cache functions # cache functions
proc updateCommunityTokenCache(self: Service, chainId: int, address: string, tokenToUpdate: CommunityTokenDto) = proc updateCommunityTokenCache(self: Service, chainId: int, address: string, tokenToUpdate: CommunityTokenDto) =
@ -507,8 +508,7 @@ QtObject:
# update owners list if airdrop was successfull # update owners list if airdrop was successfull
if signalArgs.success: if signalArgs.success:
self.tempTokenOwnersToFetch = signalArgs.communityToken self.refreshTokenHolders(signalArgs.communityToken)
self.tokenOwners1SecTimer.start()
except Exception as e: except Exception as e:
error "Error processing airdrop pending transaction event", msg=e.msg error "Error processing airdrop pending transaction event", msg=e.msg
@ -520,8 +520,7 @@ QtObject:
# update owners list if remote destruct was successfull # update owners list if remote destruct was successfull
if signalArgs.success: if signalArgs.success:
self.tempTokenOwnersToFetch = signalArgs.communityToken self.refreshTokenHolders(signalArgs.communityToken)
self.tokenOwners1SecTimer.start()
except Exception as e: except Exception as e:
error "Error processing collectible self destruct pending transaction event", msg=e.msg error "Error processing collectible self destruct pending transaction event", msg=e.msg
@ -574,24 +573,16 @@ QtObject:
proc processCommunityTokenAction(self: Service, signalArgs: CommunityTokenActionSignal) = proc processCommunityTokenAction(self: Service, signalArgs: CommunityTokenActionSignal) =
case signalArgs.actionType case signalArgs.actionType
of CommunityTokenActionType.Airdrop: of CommunityTokenActionType.Airdrop:
self.tempTokenOwnersToFetch = signalArgs.communityToken self.refreshTokenHolders(signalArgs.communityToken)
self.tokenOwners1SecTimer.start()
of CommunityTokenActionType.Burn: of CommunityTokenActionType.Burn:
self.updateCommunityTokenCache(signalArgs.communityToken.chainId, signalArgs.communityToken.address, signalArgs.communityToken) self.updateCommunityTokenCache(signalArgs.communityToken.chainId, signalArgs.communityToken.address, signalArgs.communityToken)
let data = RemoteDestructArgs(communityToken: signalArgs.communityToken) let data = RemoteDestructArgs(communityToken: signalArgs.communityToken)
self.events.emit(SIGNAL_BURN_ACTION_RECEIVED, data) self.events.emit(SIGNAL_BURN_ACTION_RECEIVED, data)
of CommunityTokenActionType.RemoteDestruct: of CommunityTokenActionType.RemoteDestruct:
self.tempTokenOwnersToFetch = signalArgs.communityToken self.refreshTokenHolders(signalArgs.communityToken)
self.tokenOwners1SecTimer.start()
else: else:
warn "Unknown token action", actionType=signalArgs.actionType warn "Unknown token action", actionType=signalArgs.actionType
proc tryFetchOwners(self: Service) =
# both communities and tokens should be loaded
if self.allCommunityTokensLoaded and self.communityDataLoaded:
self.fetchAllTokenOwners()
self.tokenOwnersTimer.start()
proc init*(self: Service) = proc init*(self: Service) =
self.getAllCommunityTokensAsync() self.getAllCommunityTokensAsync()
@ -602,10 +593,6 @@ QtObject:
elif data.eventType == tokens_backend.eventCommunityTokenReceived: elif data.eventType == tokens_backend.eventCommunityTokenReceived:
self.processReceivedCommunityTokenWalletEvent(data.message, data.accounts) self.processReceivedCommunityTokenWalletEvent(data.message, data.accounts)
self.events.on(SIGNAL_COMMUNITY_DATA_LOADED) do(e:Args):
self.communityDataLoaded = true
self.tryFetchOwners()
self.events.on(SignalType.CommunityTokenAction.event) do(e:Args): self.events.on(SignalType.CommunityTokenAction.event) do(e:Args):
let receivedData = CommunityTokenActionSignal(e) let receivedData = CommunityTokenActionSignal(e)
self.processCommunityTokenAction(receivedData) self.processCommunityTokenAction(receivedData)
@ -753,8 +740,6 @@ QtObject:
self.communityTokensCache = map(responseJson["response"]["result"].getElems(), self.communityTokensCache = map(responseJson["response"]["result"].getElems(),
proc(x: JsonNode): CommunityTokenDto = x.toCommunityTokenDto()) proc(x: JsonNode): CommunityTokenDto = x.toCommunityTokenDto())
self.allCommunityTokensLoaded = true
self.tryFetchOwners()
except RpcException as e: except RpcException as e:
error "Error getting all community tokens async", message = e.msg error "Error getting all community tokens async", message = e.msg
@ -1315,6 +1300,9 @@ QtObject:
let data = CommunityTokenOwnersArgs(chainId: chainId, contractAddress: contractAddress, communityId: communityId, owners: communityTokenOwners) let data = CommunityTokenOwnersArgs(chainId: chainId, contractAddress: contractAddress, communityId: communityId, owners: communityTokenOwners)
self.events.emit(SIGNAL_COMMUNITY_TOKEN_OWNERS_FETCHED, data) self.events.emit(SIGNAL_COMMUNITY_TOKEN_OWNERS_FETCHED, data)
# restart token holders timer
self.restartTokenHoldersTimer(chainId, contractAddress)
# get owners from cache # get owners from cache
proc getCommunityTokenOwners*(self: Service, communityId: string, chainId: int, contractAddress: string): seq[CommunityCollectibleOwner] = proc getCommunityTokenOwners*(self: Service, communityId: string, chainId: int, contractAddress: string): seq[CommunityCollectibleOwner] =
return self.tokenOwnersCache.getOrDefault((chainId: chainId, address: contractAddress)) return self.tokenOwnersCache.getOrDefault((chainId: chainId, address: contractAddress))
@ -1323,25 +1311,6 @@ QtObject:
let community = self.communityService.getCommunityById(communityId) let community = self.communityService.getCommunityById(communityId)
return community.isPrivilegedUser() return community.isPrivilegedUser()
# update in 5 minute intervals, only transferable tokens
proc onRefreshTransferableTokenOwners*(self:Service) {.slot.} =
let allTokens = self.getAllCommunityTokens()
for token in allTokens:
if token.transferable and self.iAmCommunityPrivilegedUser(token.communityId):
self.fetchCommunityOwners(token)
# used after airdrop or remote destruct
proc onFetchTempTokenOwners*(self: Service) {.slot.} =
self.fetchCommunityOwners(self.tempTokenOwnersToFetch)
# used in init
proc fetchAllTokenOwners(self: Service) =
let allTokens = self.getAllCommunityTokens()
for token in allTokens:
if not self.iAmCommunityPrivilegedUser(token.communityId):
continue
self.fetchCommunityOwners(token)
# used when community members changed # used when community members changed
proc fetchCommunityTokenOwners*(self: Service, communityId: string) = proc fetchCommunityTokenOwners*(self: Service, communityId: string) =
if not self.iAmCommunityPrivilegedUser(communityId): if not self.iAmCommunityPrivilegedUser(communityId):
@ -1409,3 +1378,59 @@ QtObject:
discard tokens_backend.reTrackOwnerTokenDeploymentTransaction(chainId, contractAddress) discard tokens_backend.reTrackOwnerTokenDeploymentTransaction(chainId, contractAddress)
except Exception: except Exception:
error "can't retrack token transaction", message = getCurrentExceptionMsg() error "can't retrack token transaction", message = getCurrentExceptionMsg()
# ran also when holders are fetched
proc restartTokenHoldersTimer(self: Service, chainId: int, contractAddress: string) =
if not self.tokenHoldersManagementStarted:
return
self.tokenHoldersTimer.stop()
let tokenTupleKey = (chainId: chainId, address: contractAddress)
var nextTimerShotInSeconds = int64(0)
if self.tokenHoldersLastUpdateMap.hasKey(tokenTupleKey):
let lastUpdateTime = self.tokenHoldersLastUpdateMap[tokenTupleKey]
const intervalInSecs = int64(5*60)
let nowInSeconds = now().toTime().toUnix()
nextTimerShotInSeconds = intervalInSecs - (nowInSeconds - lastUpdateTime)
if nextTimerShotInSeconds < 0:
nextTimerShotInSeconds = 0
self.tokenHoldersTimer.setInterval(int(nextTimerShotInSeconds * 1000))
self.tokenHoldersTimer.start()
# executed when Token page with holders is opened
proc startTokenHoldersManagement*(self: Service, chainId: int, contractAddress: string) =
let communityToken = self.getCommunityToken(chainId, contractAddress)
if not self.iAmCommunityPrivilegedUser(communityToken.communityId):
warn "can't get token holders - not privileged user"
return
self.tokenHoldersToken = communityToken
self.tokenHoldersManagementStarted = true
self.restartTokenHoldersTimer(chainId, contractAddress)
# executed when Token page with holders is closed
proc stopTokenHoldersManagement*(self: Service) =
self.tokenHoldersManagementStarted = false
self.tokenHoldersTimer.stop()
proc onTokenHoldersTimeout(self: Service) {.slot.} =
# update last fetch time
let tokenTupleKey = (chainId: self.tokenHoldersToken.chainId, address: self.tokenHoldersToken.address)
let nowInSeconds = now().toTime().toUnix()
self.tokenHoldersLastUpdateMap[tokenTupleKey] = nowInSeconds
# run async calls to fetch holders
self.fetchCommunityOwners(self.tokenHoldersToken)
# executed when there was some change and holders needs to be fetched again
proc refreshTokenHolders(self: Service, token: CommunityTokenDto) =
let tokenTupleKey = (chainId: token.chainId, address: token.address)
self.tokenHoldersLastUpdateMap.del(tokenTupleKey)
if not self.tokenHoldersManagementStarted:
# not need to get holders now
return
let holdersTokenTuple = (chainId: self.tokenHoldersToken.chainId, address: self.tokenHoldersToken.address)
if (tokenTupleKey != holdersTokenTuple):
# different token is opened now
return
self.restartTokenHoldersTimer(token.chainId, token.address)

View File

@ -79,6 +79,9 @@ StackView {
signal registerSelfDestructFeesSubscriber(var feeSubscriber) signal registerSelfDestructFeesSubscriber(var feeSubscriber)
signal registerBurnTokenFeesSubscriber(var feeSubscriber) signal registerBurnTokenFeesSubscriber(var feeSubscriber)
signal startTokenHoldersManagement(int chainId, string address)
signal stopTokenHoldersManagement()
function navigateBack() { function navigateBack() {
pop(StackView.Immediate) pop(StackView.Immediate)
} }
@ -541,6 +544,9 @@ StackView {
tokenOwnersModel: tokenViewPage.tokenOwnersModel tokenOwnersModel: tokenViewPage.tokenOwnersModel
isOwnerTokenItem: tokenViewPage.isOwnerTokenItem isOwnerTokenItem: tokenViewPage.isOwnerTokenItem
onStartTokenHoldersManagement: root.startTokenHoldersManagement(chainId, address)
onStopTokenHoldersManagement: root.stopTokenHoldersManagement()
onGeneralAirdropRequested: { onGeneralAirdropRequested: {
root.airdropToken(view.airdropKey, root.airdropToken(view.airdropKey,
"1" + "0".repeat(view.token.multiplierIndex), "1" + "0".repeat(view.token.multiplierIndex),

View File

@ -368,6 +368,10 @@ StatusSectionLayout {
onRegisterBurnTokenFeesSubscriber: d.feesBroker.registerBurnFeesSubscriber(feeSubscriber) onRegisterBurnTokenFeesSubscriber: d.feesBroker.registerBurnFeesSubscriber(feeSubscriber)
onStartTokenHoldersManagement: communityTokensStore.startTokenHoldersManagement(chainId, address)
onStopTokenHoldersManagement: communityTokensStore.stopTokenHoldersManagement()
onMintCollectible: onMintCollectible:
communityTokensStore.deployCollectible( communityTokensStore.deployCollectible(
root.community.id, collectibleItem) root.community.id, collectibleItem)

View File

@ -78,6 +78,20 @@ StatusScrollView {
signal kickRequested(string name, string contactId, string address) signal kickRequested(string name, string contactId, string address)
signal banRequested(string name, string contactId, string address) signal banRequested(string name, string contactId, string address)
signal startTokenHoldersManagement(int chainId, string address)
signal stopTokenHoldersManagement()
onVisibleChanged: {
if (visible) {
root.startTokenHoldersManagement(root.chainId, root.token.tokenAddress)
} else {
root.stopTokenHoldersManagement()
}
}
Component.onCompleted: root.startTokenHoldersManagement(root.chainId, root.token.tokenAddress)
Component.onDestruction: root.stopTokenHoldersManagement()
QtObject { QtObject {
id: d id: d

View File

@ -220,4 +220,12 @@ QtObject {
function asyncGetOwnerTokenDetails(communityId) { function asyncGetOwnerTokenDetails(communityId) {
communityTokensModuleInst.asyncGetOwnerTokenDetails(communityId) communityTokensModuleInst.asyncGetOwnerTokenDetails(communityId)
} }
function startTokenHoldersManagement(chainId, contractAddress) {
communityTokensModuleInst.startTokenHoldersManagement(chainId, contractAddress)
}
function stopTokenHoldersManagement() {
communityTokensModuleInst.stopTokenHoldersManagement()
}
} }