diff --git a/src/app/modules/main/wallet_section/collectibles/controller.nim b/src/app/modules/main/wallet_section/collectibles/controller.nim index cfb06272b4..b95a40399d 100644 --- a/src/app/modules/main/wallet_section/collectibles/controller.nim +++ b/src/app/modules/main/wallet_section/collectibles/controller.nim @@ -30,11 +30,22 @@ proc newController*( proc delete*(self: Controller) = discard -proc refreshCollectibles*(self: Controller, chainId: int, address: string) = - let collectibles = self.collectibleService.getOwnedCollectibles(chainId, address) - self.delegate.refreshCollectibles(chainId, address, collectibles) +proc refreshCollectibles(self: Controller, chainId: int, address: string) = + let data = self.collectibleService.getOwnedCollectibles(chainId, address) + if not data.anyLoaded or data.lastLoadWasFromStart: + self.delegate.setCollectibles(chainId, address, data) + else: + self.delegate.appendCollectibles(chainId, address, data) proc init*(self: Controller) = + self.events.on(SIGNAL_OWNED_COLLECTIBLES_RESET) do(e:Args): + let args = OwnedCollectiblesUpdateArgs(e) + self.refreshCollectibles(args.chainId, args.address) + + self.events.on(SIGNAL_OWNED_COLLECTIBLES_UPDATE_STARTED) do(e:Args): + let args = OwnedCollectiblesUpdateArgs(e) + self.delegate.onFetchStarted(args.chainId, args.address) + self.events.on(SIGNAL_OWNED_COLLECTIBLES_UPDATE_FINISHED) do(e:Args): let args = OwnedCollectiblesUpdateArgs(e) self.refreshCollectibles(args.chainId, args.address) @@ -45,11 +56,14 @@ proc getWalletAccount*(self: Controller, accountIndex: int): wallet_account_serv proc getNetwork*(self: Controller): network_service.NetworkDto = return self.networkService.getNetworkForCollectibles() +proc getOwnedCollectibles*(self: Controller, chainId: int, address: string): CollectiblesData = + return self.collectibleService.getOwnedCollectibles(chainId, address) + proc resetOwnedCollectibles*(self: Controller, chainId: int, address: string) = self.collectibleService.resetOwnedCollectibles(chainId, address) -proc fetchOwnedCollectibles*(self: Controller, chainId: int, address: string, limit: int) = - self.collectibleService.fetchOwnedCollectibles(chainId, address, limit) +proc fetchOwnedCollectibles*(self: Controller, chainId: int, address: string) = + self.collectibleService.fetchOwnedCollectibles(chainId, address) proc getCollectible*(self: Controller, chainId: int, id: UniqueID) : CollectibleDto = self.collectibleService.getCollectible(chainId, id) diff --git a/src/app/modules/main/wallet_section/collectibles/io_interface.nim b/src/app/modules/main/wallet_section/collectibles/io_interface.nim index 3a01f670e6..83b839e36f 100644 --- a/src/app/modules/main/wallet_section/collectibles/io_interface.nim +++ b/src/app/modules/main/wallet_section/collectibles/io_interface.nim @@ -16,10 +16,16 @@ method isLoaded*(self: AccessInterface): bool {.base.} = method switchAccount*(self: AccessInterface, accountIndex: int) {.base.} = raise newException(ValueError, "No implementation available") -method fetchOwnedCollectibles*(self: AccessInterface, limit: int) {.base.} = +method fetchOwnedCollectibles*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") -method refreshCollectibles*(self: AccessInterface, chainId: int, address: string, collectibles: CollectiblesData) {.base.} = +method onFetchStarted*(self: AccessInterface, chainId: int, address: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method setCollectibles*(self: AccessInterface, chainId: int, address: string, data: CollectiblesData) {.base.} = + raise newException(ValueError, "No implementation available") + +method appendCollectibles*(self: AccessInterface, chainId: int, address: string, data: CollectiblesData) {.base.} = raise newException(ValueError, "No implementation available") method viewDidLoad*(self: AccessInterface) {.base.} = diff --git a/src/app/modules/main/wallet_section/collectibles/models/collectibles_item.nim b/src/app/modules/main/wallet_section/collectibles/models/collectibles_item.nim index ceb8ad148d..2a4cc4f815 100644 --- a/src/app/modules/main/wallet_section/collectibles/models/collectibles_item.nim +++ b/src/app/modules/main/wallet_section/collectibles/models/collectibles_item.nim @@ -18,6 +18,7 @@ type collectionSlug: string collectionImageUrl: string isLoading: bool + isPinned: bool proc initItem*( id: int, @@ -33,7 +34,8 @@ proc initItem*( stats: seq[CollectibleTrait], collectionName: string, collectionSlug: string, - collectionImageUrl: string + collectionImageUrl: string, + isPinned: bool ): Item = result.id = id result.address = address @@ -50,9 +52,10 @@ proc initItem*( result.collectionSlug = collectionSlug result.collectionImageUrl = collectionImageUrl result.isLoading = false + result.isPinned = isPinned proc initItem*: Item = - result = initItem(-1, "", u256(0), "", "", "transparent", "Collectibles", "", @[], @[], @[], "", "", "") + result = initItem(-1, "", u256(0), "", "", "transparent", "Collectibles", "", @[], @[], @[], "", "", "", false) proc initLoadingItem*: Item = result = initItem() @@ -71,6 +74,8 @@ proc `$`*(self: Item): string = collectionName: {self.collectionName}, collectionSlug: {self.collectionSlug}, collectionImageUrl: {self.collectionImageUrl}, + isLoading: {self.isLoading}, + isPinned: {self.isPinned}, ]""" proc getId*(self: Item): int = @@ -117,3 +122,6 @@ proc getCollectionImageUrl*(self: Item): string = proc getIsLoading*(self: Item): bool = return self.isLoading + +proc getIsPinned*(self: Item): bool = + return self.isPinned diff --git a/src/app/modules/main/wallet_section/collectibles/models/collectibles_model.nim b/src/app/modules/main/wallet_section/collectibles/models/collectibles_model.nim index 98ec673f6c..20cf4cc75d 100644 --- a/src/app/modules/main/wallet_section/collectibles/models/collectibles_model.nim +++ b/src/app/modules/main/wallet_section/collectibles/models/collectibles_model.nim @@ -19,9 +19,9 @@ type CollectionSlug CollectionImageUrl IsLoading + IsPinned -# Maximum number of owned collectibles to be fetched at a time -const ownedCollectiblesFetchLimit = 200 +const loadingItemsCount = 50 QtObject: type @@ -64,7 +64,7 @@ QtObject: QtProperty[bool] isFetching: read = getIsFetching notify = isFetchingChanged - proc setIsFetching(self: Model, value: bool) = + proc setIsFetching*(self: Model, value: bool) = if value == self.isFetching: return if value: @@ -89,11 +89,10 @@ QtObject: method canFetchMore*(self: Model, parent: QModelIndex): bool = return not self.allCollectiblesLoaded and not self.isFetching - proc requestFetch(self: Model, limit: int) {.signal.} + proc requestFetch(self: Model) {.signal.} method fetchMore*(self: Model, parent: QModelIndex) = if not self.isFetching: - self.setIsFetching(true) - self.requestFetch(ownedCollectiblesFetchLimit) + self.requestFetch() method rowCount*(self: Model, index: QModelIndex = nil): int = return self.items.len @@ -115,6 +114,7 @@ QtObject: CollectibleRole.CollectionSlug.int:"collectionSlug", CollectibleRole.CollectionImageUrl.int:"collectionImageUrl", CollectibleRole.IsLoading.int:"isLoading", + CollectibleRole.IsPinned.int:"isPinned", }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -164,37 +164,42 @@ QtObject: result = newQVariant(item.getCollectionImageUrl()) of CollectibleRole.IsLoading: result = newQVariant(item.getIsLoading()) + of CollectibleRole.IsPinned: + result = newQVariant(item.getIsPinned()) proc addLoadingItems(self: Model) = let parentModelIndex = newQModelIndex() defer: parentModelIndex.delete let loadingItem = initLoadingItem() - self.beginInsertRows(parentModelIndex, self.items.len, self.items.len + ownedCollectiblesFetchLimit - 1) - for i in 1..ownedCollectiblesFetchLimit: + self.beginInsertRows(parentModelIndex, self.items.len, self.items.len + loadingItemsCount - 1) + for i in 1..loadingItemsCount: self.items.add(loadingItem) self.endInsertRows() self.countChanged() proc removeLoadingItems(self: Model) = + let parentModelIndex = newQModelIndex() + defer: parentModelIndex.delete + for i in 0 ..< self.items.len: if self.items[i].getIsLoading(): - self.beginRemoveRows(newQModelIndex(), i, self.items.len-1) - self.items.delete(i, self.items.len-1) + self.beginRemoveRows(parentModelIndex, i, i + loadingItemsCount - 1) + self.items.delete(i, i + loadingItemsCount - 1) self.endRemoveRows() self.countChanged() return - proc setItems*(self: Model, items: seq[Item], append: bool) = - self.setIsFetching(false) - if append: - let parentModelIndex = newQModelIndex() - defer: parentModelIndex.delete - self.beginInsertRows(parentModelIndex, self.items.len, self.items.len + items.len - 1) - self.items = concat(self.items, items) - self.endInsertRows() - else: - self.beginResetModel() - self.items = items - self.endResetModel() + proc setItems*(self: Model, items: seq[Item]) = + self.beginResetModel() + self.items = items + self.endResetModel() + self.countChanged() + + proc appendItems*(self: Model, items: seq[Item]) = + let parentModelIndex = newQModelIndex() + defer: parentModelIndex.delete + self.beginInsertRows(parentModelIndex, self.items.len, self.items.len + items.len - 1) + self.items = concat(self.items, items) + self.endInsertRows() self.countChanged() diff --git a/src/app/modules/main/wallet_section/collectibles/models/collectibles_utils.nim b/src/app/modules/main/wallet_section/collectibles/models/collectibles_utils.nim index 8a5c20c9dd..74259f600b 100644 --- a/src/app/modules/main/wallet_section/collectibles/models/collectibles_utils.nim +++ b/src/app/modules/main/wallet_section/collectibles/models/collectibles_utils.nim @@ -1,8 +1,8 @@ -import sequtils, sugar +import sequtils, sugar, times import ../../../../../../app_service/service/collectible/dto import collectibles_item, collectible_trait_item -proc collectibleToItem*(c: CollectibleDto, co: CollectionDto) : Item = +proc collectibleToItem*(c: CollectibleDto, co: CollectionDto, isPinned: bool = false) : Item = return initItem( c.id, c.address, @@ -17,5 +17,6 @@ proc collectibleToItem*(c: CollectibleDto, co: CollectionDto) : Item = c.statistics.map(t => initTrait(t.traitType, t.value, t.displayType, t.maxValue)), co.name, co.slug, - co.imageUrl + co.imageUrl, + isPinned ) diff --git a/src/app/modules/main/wallet_section/collectibles/module.nim b/src/app/modules/main/wallet_section/collectibles/module.nim index e12e76c4a2..a1271e3fc2 100644 --- a/src/app/modules/main/wallet_section/collectibles/module.nim +++ b/src/app/modules/main/wallet_section/collectibles/module.nim @@ -73,8 +73,8 @@ method viewDidLoad*(self: Module) = method currentCollectibleModuleDidLoad*(self: Module) = self.checkIfModuleDidLoad() -method fetchOwnedCollectibles*(self: Module, limit: int) = - self.controller.fetchOwnedCollectibles(self.chainId, self.address, limit) +method fetchOwnedCollectibles*(self: Module) = + self.controller.fetchOwnedCollectibles(self.chainId, self.address) method switchAccount*(self: Module, accountIndex: int) = let network = self.controller.getNetwork() @@ -83,30 +83,42 @@ method switchAccount*(self: Module, accountIndex: int) = self.chainId = network.chainId self.address = account.address - # TODO: Implement a way to reduce the number of full re-fetches. It could be only - # when NFT activity was detected for the given account, or if a certain amount of - # time has passed. For now, we fetch every time we select the account. - self.controller.resetOwnedCollectibles(self.chainId, self.address) - - self.controller.refreshCollectibles(self.chainId, self.address) - self.currentCollectibleModule.setCurrentAddress(network, self.address) -method refreshCollectibles*(self: Module, chainId: int, address: string, collectibles: CollectiblesData) = + let data = self.controller.getOwnedCollectibles(self.chainId, self.address) + + # Trigger a fetch the first time we switch to an account + if not data.anyLoaded: + self.controller.fetchOwnedCollectibles(self.chainId, self.address) + + self.setCollectibles(self.chainId, self.address, data) + +proc ownedCollectibleToItem(self: Module, oc: OwnedCollectible): Item = + let c = self.controller.getCollectible(self.chainId, oc.id) + let col = self.controller.getCollection(self.chainId, c.collectionSlug) + return collectibleToItem(c, col, oc.isFromWatchedContract) + +method onFetchStarted*(self: Module, chainId: int, address: string) = if self.chainId == chainId and self.address == address: - var idsToAdd = newSeq[UniqueID]() - let append = not collectibles.lastLoadWasFromStart + self.view.setIsFetching(true) - var startIdx = 0 - if append: - for i in collectibles.ids.len - collectibles.lastLoadCount ..< collectibles.ids.len: - idsToAdd.add(collectibles.ids[i]) - else: - idsToAdd = collectibles.ids +method setCollectibles*(self: Module, chainId: int, address: string, data: CollectiblesData) = + if self.chainId == chainId and self.address == address: + self.view.setIsFetching(data.isFetching) + var newCollectibles = data.collectibles.map(oc => self.ownedCollectibleToItem(oc)) + self.view.setCollectibles(newCollectibles) + self.view.setAllLoaded(data.allLoaded) - var newCollectibles = idsToAdd.map(id => (block: - let c = self.controller.getCollectible(self.chainId, id) - let co = self.controller.getCollection(self.chainId, c.collectionSlug) - return collectibleToItem(c, co) - )) - self.view.setCollectibles(newCollectibles, append, collectibles.allLoaded) + +method appendCollectibles*(self: Module, chainId: int, address: string, data: CollectiblesData) = + if self.chainId == chainId and self.address == address: + self.view.setIsFetching(data.isFetching) + + var ownedCollectiblesToAdd = newSeq[OwnedCollectible]() + for i in data.collectibles.len - data.lastLoadCount ..< data.collectibles.len: + ownedCollectiblesToAdd.add(data.collectibles[i]) + + let newCollectibles = ownedCollectiblesToAdd.map(oc => self.ownedCollectibleToItem(oc)) + + self.view.appendCollectibles(newCollectibles) + self.view.setAllLoaded(data.allLoaded) diff --git a/src/app/modules/main/wallet_section/collectibles/view.nim b/src/app/modules/main/wallet_section/collectibles/view.nim index bf0a78ada1..d40be2de42 100644 --- a/src/app/modules/main/wallet_section/collectibles/view.nim +++ b/src/app/modules/main/wallet_section/collectibles/view.nim @@ -19,7 +19,7 @@ QtObject: result.QObject.setup result.delegate = delegate result.model = newModel() - signalConnect(result.model, "requestFetch(int)", result, "fetchMoreOwnedCollectibles(int)") + signalConnect(result.model, "requestFetch()", result, "fetchMoreOwnedCollectibles()") proc load*(self: View) = self.delegate.viewDidLoad() @@ -31,9 +31,17 @@ QtObject: read = getModel notify = modelChanged - proc fetchMoreOwnedCollectibles*(self: View, limit: int) {.slot.} = - self.delegate.fetchOwnedCollectibles(limit) + proc fetchMoreOwnedCollectibles*(self: View) {.slot.} = + self.delegate.fetchOwnedCollectibles() - proc setCollectibles*(self: View, collectibles: seq[Item], append: bool, allLoaded: bool) = - self.model.setItems(collectibles, append) + proc setIsFetching*(self: View, isFetching: bool) = + self.model.setIsFetching(isFetching) + + proc setAllLoaded*(self: View, allLoaded: bool) = self.model.setAllCollectiblesLoaded(allLoaded) + + proc setCollectibles*(self: View, collectibles: seq[Item]) = + self.model.setItems(collectibles) + + proc appendCollectibles*(self: View, collectibles: seq[Item]) = + self.model.appendItems(collectibles) diff --git a/src/app_service/service/collectible/async_tasks.nim b/src/app_service/service/collectible/async_tasks.nim index 8778e783c5..e934af4f02 100644 --- a/src/app_service/service/collectible/async_tasks.nim +++ b/src/app_service/service/collectible/async_tasks.nim @@ -21,6 +21,30 @@ const fetchOwnedCollectiblesTaskArg: Task = proc(argEncoded: string) {.gcsafe, n error "error fetchOwnedCollectiblesTaskArg: ", errDesription arg.finish(output) +type + FetchOwnedCollectiblesFromContractAddressesTaskArg = ref object of QObjectTaskArg + chainId*: int + address*: string + contractAddresses*: seq[string] + cursor: string + limit: int + +const fetchOwnedCollectiblesFromContractAddressesTaskArg: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[FetchOwnedCollectiblesFromContractAddressesTaskArg](argEncoded) + let output = %* { + "chainId": arg.chainId, + "address": arg.address, + "cursor": arg.cursor, + "collectibles": "" + } + try: + let response = collectibles.getOpenseaAssetsByOwnerAndContractAddressWithCursor(arg.chainId, arg.address, arg.contractAddresses, arg.cursor, arg.limit) + output["collectibles"] = response.result + except Exception as e: + let errDesription = e.msg + error "error fetchOwnedCollectiblesFromContractAddressesTaskArg: ", errDesription + arg.finish(output) + type FetchCollectiblesTaskArg = ref object of QObjectTaskArg chainId*: int diff --git a/src/app_service/service/collectible/dto.nim b/src/app_service/service/collectible/dto.nim index ec3ee2fa96..693a3683de 100644 --- a/src/app_service/service/collectible/dto.nim +++ b/src/app_service/service/collectible/dto.nim @@ -1,4 +1,4 @@ -import json, Tables, stint, strformat, strutils +import json, Tables, stint, strformat, strutils, times # Unique identifier for collectible on a specific chain type diff --git a/src/app_service/service/collectible/service.nim b/src/app_service/service/collectible/service.nim index c4dc8b9c96..ba929bcc2b 100644 --- a/src/app_service/service/collectible/service.nim +++ b/src/app_service/service/collectible/service.nim @@ -1,5 +1,6 @@ -import NimQml, Tables, chronicles, sequtils, json, sugar, stint, hashes, strformat, times +import NimQml, Tables, chronicles, sequtils, json, sugar, stint, hashes, strformat, times, strutils import ../../../app/core/eventemitter +import ../../../app/core/signals/types import ../../../app/core/tasks/[qt, threadpool] import dto @@ -16,10 +17,17 @@ logScope: topics = "collectible-service" # Signals which may be emitted by this service: +const SIGNAL_OWNED_COLLECTIBLES_RESET* = "ownedCollectiblesReset" const SIGNAL_OWNED_COLLECTIBLES_UPDATE_STARTED* = "ownedCollectiblesUpdateStarted" const SIGNAL_OWNED_COLLECTIBLES_UPDATE_FINISHED* = "ownedCollectiblesUpdateFinished" +const SIGNAL_OWNED_COLLECTIBLES_FROM_WATCHED_CONTRACTS_FETCHED* = "ownedCollectiblesFromWatchedContractsFetched" const SIGNAL_COLLECTIBLES_UPDATED* = "collectiblesUpdated" +const INVALID_TIMESTAMP* = fromUnix(0) + +# Maximum number of owned collectibles to be fetched at a time +const ownedCollectiblesFetchLimit = 200 + type OwnedCollectiblesUpdateArgs* = ref object of Args chainId*: int @@ -30,45 +38,77 @@ type chainId*: int ids*: seq[UniqueID] +type + OwnedCollectible* = ref object of Args + id*: UniqueID + isFromWatchedContract*: bool + +proc `$`*(self: OwnedCollectible): string = + return fmt"""OwnedCollectible( + id:{self.id}, + isFromWatchedContract:{self.isFromWatchedContract} + )""" + type CollectiblesData* = ref object isFetching*: bool + anyLoaded*: bool allLoaded*: bool lastLoadWasFromStart*: bool lastLoadFromStartTimestamp*: DateTime lastLoadCount*: int previousCursor*: string nextCursor*: string - ids*: seq[UniqueID] + collectibles*: seq[OwnedCollectible] + collectiblesFromWatchedContracts: seq[OwnedCollectible] -proc newCollectiblesData*(): CollectiblesData = +proc newCollectiblesData(): CollectiblesData = new(result) result.isFetching = false + result.anyLoaded = false result.allLoaded = false result.lastLoadWasFromStart = false - result.lastLoadFromStartTimestamp = now() - initDuration(weeks = 1) + result.lastLoadFromStartTimestamp = INVALID_TIMESTAMP.utc() result.lastLoadCount = 0 result.previousCursor = "" result.nextCursor = "" - result.ids = @[] + result.collectibles = @[] + result.collectiblesFromWatchedContracts = @[] proc `$`*(self: CollectiblesData): string = return fmt"""CollectiblesData( isFetching:{self.isFetching}, + anyLoaded:{self.anyLoaded}, allLoaded:{self.allLoaded}, lastLoadWasFromStart:{self.lastLoadWasFromStart}, lastLoadFromStartTimestamp:{self.lastLoadFromStartTimestamp}, lastLoadCount:{self.lastLoadCount}, previousCursor:{self.previousCursor}, nextCursor:{self.nextCursor}, - ids:{self.ids} + collectibles:{self.collectibles} )""" type - AdressesData = TableRef[string, CollectiblesData] # [address, CollectiblesData] + OwnershipData* = ref object + data*: CollectiblesData + watchedContractAddresses*: seq[string] + +proc newOwnershipData(): OwnershipData = + new(result) + result.data = newCollectiblesData() + result.watchedContractAddresses = @[] type - ChainsData = TableRef[int, AdressesData] # [chainId, AdressesData] + AddressesData = TableRef[string, OwnershipData] # [address, OwnershipData] + +proc newAddressesData(): AddressesData = + result = newTable[string, OwnershipData]() + +type + ChainsData = TableRef[int, AddressesData] # [chainId, AddressesData] + +proc newChainsData(): ChainsData = + result = newTable[int, AddressesData]() type CollectiblesResult = tuple[success: bool, collectibles: seq[CollectibleDto], collections: seq[CollectionDto], previousCursor: string, nextCursor: string] @@ -83,10 +123,14 @@ QtObject: events: EventEmitter threadpool: ThreadPool networkService: network_service.Service - ownershipData: ChainsData + accountsOwnershipData: ChainsData collectibles: TableRef[int, TableRef[UniqueID, CollectibleDto]] # [chainId, [UniqueID, CollectibleDto]] collections: TableRef[int, TableRef[string, CollectionDto]] # [chainId, [slug, CollectionDto]] + # Forward declarations + proc resetOwnedCollectibles*(self: Service, chainId: int, address: string) + proc resetAllOwnedCollectibles*(self: Service) + proc delete*(self: Service) = self.QObject.delete @@ -100,33 +144,49 @@ QtObject: result.events = events result.threadpool = threadpool result.networkService = networkService - result.ownershipData = newTable[int, AdressesData]() + result.accountsOwnershipData = newChainsData() result.collectibles = newTable[int, TableRef[UniqueID, CollectibleDto]]() result.collections = newTable[int, TableRef[string, CollectionDto]]() proc init*(self: Service) = - discard + self.events.on(SignalType.Wallet.event) do(e:Args): + var data = WalletSignal(e) + case data.eventType: + of "wallet-tick-reload": + self.resetAllOwnedCollectibles() - proc prepareOwnershipData(self: Service, chainId: int, address: string, reset: bool = false) = - if not self.ownershipData.hasKey(chainId): - self.ownershipData[chainId] = newTable[string, CollectiblesData]() + proc prepareOwnershipData(self: Service, chainId: int, address: string) = + if not self.accountsOwnershipData.hasKey(chainId): + self.accountsOwnershipData[chainId] = newAddressesData() - let chainData = self.ownershipData[chainId] - if reset or not chainData.hasKey(address): - chainData[address] = newCollectiblesData() + let chainData = self.accountsOwnershipData[chainId] + if not chainData.hasKey(address): + chainData[address] = newOwnershipData() - let addressData = self.ownershipData[chainId][address] - addressData.lastLoadWasFromStart = reset - if reset: - addressData.lastLoadFromStartTimestamp = now() - - - proc updateOwnedCollectibles(self: Service, chainId: int, address: string, previousCursor: string, nextCursor: string, collectibles: seq[CollectibleDto]) = + proc updateOwnedCollectibles(self: Service, chainId: int, address: string, previousCursor: string, nextCursor: string, collectibles: seq[CollectibleDto], isFromWatchedContract: bool) = + let ownershipData = self.accountsOwnershipData[chainId][address] + let collectiblesData = ownershipData.data try: - let collectiblesData = self.ownershipData[chainId][address] - collectiblesData.previousCursor = previousCursor - collectiblesData.nextCursor = nextCursor - collectiblesData.allLoaded = (nextCursor == "") + if not (collectiblesData.nextCursor == previousCursor): + # Async response from an old fetch request, disregard + return + if not collectiblesData.anyLoaded: + collectiblesData.lastLoadWasFromStart = true + collectiblesData.lastLoadFromStartTimestamp = now() + else: + collectiblesData.lastLoadWasFromStart = false + + collectiblesData.anyLoaded = true + + if isFromWatchedContract: + # All fetched in one go, ignore cursors + collectiblesData.previousCursor = "" + collectiblesData.nextCursor = "" + collectiblesData.allLoaded = false + else: + collectiblesData.previousCursor = previousCursor + collectiblesData.nextCursor = nextCursor + collectiblesData.allLoaded = (nextCursor == "") var count = 0 for collectible in collectibles: @@ -134,8 +194,15 @@ QtObject: contractAddress: collectible.address, tokenId: collectible.tokenId ) - if not collectiblesData.ids.any(id => newId == id): - collectiblesData.ids.add(newId) + if not collectiblesData.collectibles.any(c => newId == c.id): + let ownedCollectible = OwnedCollectible( + id: newId, + isFromWatchedContract: isFromWatchedContract + ) + collectiblesData.collectibles.add(ownedCollectible) + if isFromWatchedContract: + collectiblesData.collectiblesFromWatchedContracts.add(ownedCollectible) + count = count + 1 collectiblesData.lastLoadCount = count except Exception as e: @@ -156,7 +223,6 @@ QtObject: let slug = collection.slug self.collections[chainId][slug] = collection - for collectible in collectibles: let id = UniqueID( contractAddress: collectible.address, @@ -167,12 +233,15 @@ QtObject: self.events.emit(SIGNAL_COLLECTIBLES_UPDATED, data) + proc setWatchedContracts*(self: Service, chainId: int, address: string, contractAddresses: seq[string]) = + self.prepareOwnershipData(chainId, address) + self.accountsOwnershipData[chainId][address].watchedContractAddresses = contractAddresses + # Re-fetch + self.resetOwnedCollectibles(chainId, address) + proc getOwnedCollectibles*(self: Service, chainId: int, address: string) : CollectiblesData = - try: - return self.ownershipData[chainId][address] - except: - discard - return newCollectiblesData() + self.prepareOwnershipData(chainId, address) + return self.accountsOwnershipData[chainId][address].data proc getCollectible*(self: Service, chainId: int, id: UniqueID) : CollectibleDto = try: @@ -238,7 +307,7 @@ QtObject: limit: len(ids) ) self.threadpool.start(arg) - + proc onRxOwnedCollectibles(self: Service, response: string) {.slot.} = var data = OwnedCollectiblesUpdateArgs() try: @@ -250,27 +319,80 @@ QtObject: addressJson.kind == JString): data.chainId = chainIdJson.getInt() data.address = addressJson.getStr() - self.ownershipData[data.chainId][data.address].isFetching = false + let collectiblesData = self.accountsOwnershipData[data.chainId][data.address].data + collectiblesData.isFetching = false let (success, collectibles, collections, prevCursor, nextCursor) = processCollectiblesResult(responseObj) if success: self.updateCollectiblesCache(data.chainId, collectibles, collections) - self.updateOwnedCollectibles(data.chainId, data.address, prevCursor, nextCursor, collectibles) + self.updateOwnedCollectibles(data.chainId, data.address, prevCursor, nextCursor, collectibles, false) except Exception as e: let errDescription = e.msg error "error onRxOwnedCollectibles: ", errDescription self.events.emit(SIGNAL_OWNED_COLLECTIBLES_UPDATE_FINISHED, data) - proc resetOwnedCollectibles*(self: Service, chainId: int, address: string) = - self.prepareOwnershipData(chainId, address, true) + proc fetchNextOwnedCollectiblesChunk(self: Service, chainId: int, address: string, limit: int = ownedCollectiblesFetchLimit) = + self.prepareOwnershipData(chainId, address) + + let ownershipData = self.accountsOwnershipData[chainId][address] + let collectiblesData = ownershipData.data + + var cursor = collectiblesData.nextCursor + + let arg = FetchOwnedCollectiblesTaskArg( + tptr: cast[ByteAddress](fetchOwnedCollectiblesTaskArg), + vptr: cast[ByteAddress](self.vptr), + slot: "onRxOwnedCollectibles", + chainId: chainId, + address: address, + cursor: cursor, + limit: limit + ) + self.threadpool.start(arg) + + proc onRxOwnedCollectiblesFromWatchedContractAddresses(self: Service, response: string) {.slot.} = var data = OwnedCollectiblesUpdateArgs() - data.chainId = chainId - data.address = address + try: + let responseObj = response.parseJson + if (responseObj.kind == JObject): + let chainIdJson = responseObj["chainId"] + let addressJson = responseObj["address"] + if (chainIdJson.kind == JInt and + addressJson.kind == JString): + data.chainId = chainIdJson.getInt() + data.address = addressJson.getStr() + let collectiblesData = self.accountsOwnershipData[data.chainId][data.address].data + collectiblesData.isFetching = false + let (success, collectibles, collections, prevCursor, nextCursor) = processCollectiblesResult(responseObj) + if success: + self.updateCollectiblesCache(data.chainId, collectibles, collections) + self.updateOwnedCollectibles(data.chainId, data.address, prevCursor, nextCursor, collectibles, true) + except Exception as e: + let errDescription = e.msg + error "error onRxOwnedCollectiblesFromWatchedContractAddresses: ", errDescription + self.events.emit(SIGNAL_OWNED_COLLECTIBLES_FROM_WATCHED_CONTRACTS_FETCHED, data) self.events.emit(SIGNAL_OWNED_COLLECTIBLES_UPDATE_FINISHED, data) - proc fetchOwnedCollectibles*(self: Service, chainId: int, address: string, limit: int) = - self.prepareOwnershipData(chainId, address, false) + proc fetchOwnedCollectiblesFromWatchedContracts(self: Service, chainId: int, address: string) = + let watchedContractAddresses = self.accountsOwnershipData[chainId][address].watchedContractAddresses - let collectiblesData = self.ownershipData[chainId][address] + let arg = FetchOwnedCollectiblesFromContractAddressesTaskArg( + tptr: cast[ByteAddress](fetchOwnedCollectiblesFromContractAddressesTaskArg), + vptr: cast[ByteAddress](self.vptr), + slot: "onRxOwnedCollectiblesFromWatchedContractAddresses", + chainId: chainId, + address: address, + contractAddresses: watchedContractAddresses, + cursor: "", # Always fetch from the beginning + limit: 0 # Always fetch the complete list + ) + self.threadpool.start(arg) + + proc fetchOwnedCollectibles*(self: Service, chainId: int, address: string, limit: int = ownedCollectiblesFetchLimit) = + self.prepareOwnershipData(chainId, address) + + let ownershipData = self.accountsOwnershipData[chainId][address] + let watchedContractAddresses = ownershipData.watchedContractAddresses + let collectiblesData = ownershipData.data if collectiblesData.isFetching: return @@ -279,19 +401,31 @@ QtObject: return collectiblesData.isFetching = true - var data = OwnedCollectiblesUpdateArgs() data.chainId = chainId data.address = address self.events.emit(SIGNAL_OWNED_COLLECTIBLES_UPDATE_STARTED, data) - let arg = FetchOwnedCollectiblesTaskArg( - tptr: cast[ByteAddress](fetchOwnedCollectiblesTaskArg), - vptr: cast[ByteAddress](self.vptr), - slot: "onRxOwnedCollectibles", - chainId: chainId, - address: address, - cursor: collectiblesData.nextCursor, - limit: limit - ) - self.threadpool.start(arg) + # Full list of collectibles from watched contracts always get loaded first + if not collectiblesData.anyLoaded and len(watchedContractAddresses) > 0: + self.fetchOwnedCollectiblesFromWatchedContracts(chainId, address) + else: + self.fetchNextOwnedCollectiblesChunk(chainId, address) + + proc resetOwnedCollectibles*(self: Service, chainId: int, address: string) = + self.prepareOwnershipData(chainId, address) + + let ownershipData = self.accountsOwnershipData[chainId][address] + ownershipData.data = newCollectiblesData() + + var data = OwnedCollectiblesUpdateArgs() + data.chainId = chainId + data.address = address + self.events.emit(SIGNAL_OWNED_COLLECTIBLES_RESET, data) + + self.fetchOwnedCollectibles(chainId, address) + + proc resetAllOwnedCollectibles*(self: Service) = + for chainId, addressesData in self.accountsOwnershipData: + for address, _ in addressesData: + self.resetOwnedCollectibles(chainId, address) \ No newline at end of file diff --git a/src/backend/collectibles.nim b/src/backend/collectibles.nim index 63474c0ad6..f0b4bdd344 100644 --- a/src/backend/collectibles.nim +++ b/src/backend/collectibles.nim @@ -24,6 +24,13 @@ rpc(getOpenseaAssetsByOwnerWithCursor, "wallet"): cursor: string limit: int +rpc(getOpenseaAssetsByOwnerAndContractAddressWithCursor, "wallet"): + chainId: int + address: string + contractAddresses: seq[string] + cursor: string + limit: int + rpc(getOpenseaAssetsByNFTUniqueID, "wallet"): chainId: int uniqueIds: seq[NFTUniqueID] diff --git a/vendor/status-go b/vendor/status-go index 1453f5a0e3..bd82250cf7 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit 1453f5a0e3a2481a893ed7a233f40137eec9e9a1 +Subproject commit bd82250cf775e2f0cfd6acb37eb04349c52bd8a4