@bug(wallet/activity): Implemented collectibles model (#12294)

This commit is contained in:
Cuteivist 2023-10-03 14:15:11 +02:00 committed by GitHub
parent c155c9c9d0
commit 158bb87b4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 421 additions and 13 deletions

View File

@ -0,0 +1,61 @@
import strformat, stint
import backend/activity as backend
type
CollectibleItem* = object
chainId: int
contractAddress: string
tokenId: UInt256
name: string
imageUrl: string
proc initItem*(
chainId: int,
contractAddress: string,
tokenId: UInt256,
name: string,
imageUrl: string
): CollectibleItem =
result.chainId = chainId
result.contractAddress = contractAddress
result.tokenId = tokenId
result.name = if (name != ""): name else: ("#" & tokenId.toString())
result.imageUrl = imageUrl
proc collectibleToItem*(c: backend.CollectibleHeader) : CollectibleItem =
return initItem(
c.id.contractID.chainID,
c.id.contractID.address,
c.id.tokenID,
c.name,
c.imageUrl
)
proc `$`*(self: CollectibleItem): string =
result = fmt"""Collectibles(
chainId: {self.chainId},
contractAddress: {self.contractAddress},
tokenId: {self.tokenId},
name: {self.name},
imageUrl: {self.imageUrl}
]"""
proc getChainId*(self: CollectibleItem): int =
return self.chainId
proc getContractAddress*(self: CollectibleItem): string =
return self.contractAddress
proc getTokenId*(self: CollectibleItem): UInt256 =
return self.tokenId
# Unique ID to identify collectible, generated by us
proc getId*(self: CollectibleItem): string =
return fmt"{self.getChainId}+{self.getContractAddress}+{self.getTokenID}"
proc getName*(self: CollectibleItem): string =
return self.name
proc getImageUrl*(self: CollectibleItem): string =
return self.imageUrl

View File

@ -0,0 +1,219 @@
import NimQml, Tables, strutils, strformat, sequtils, stint
import logging
import ./collectibles_item
import web3/ethtypes as eth
import backend/activity as backend_activity
type
CollectibleRole* {.pure.} = enum
Uid = UserRole + 1,
ChainId
ContractAddress
TokenId
Name
ImageUrl
QtObject:
type
CollectiblesModel* = ref object of QAbstractListModel
items: seq[CollectibleItem]
hasMore: bool
proc delete(self: CollectiblesModel) =
self.items = @[]
self.QAbstractListModel.delete
proc setup(self: CollectiblesModel) =
self.QAbstractListModel.setup
proc newCollectiblesModel*(): CollectiblesModel =
new(result, delete)
result.setup
result.items = @[]
result.hasMore = true
proc `$`*(self: CollectiblesModel): string =
for i in 0 ..< self.items.len:
result &= fmt"""[{i}]:({$self.items[i]})"""
proc getCollectiblesCount*(self: CollectiblesModel): int =
return self.items.len
proc countChanged(self: CollectiblesModel) {.signal.}
proc getCount*(self: CollectiblesModel): int {.slot.} =
return self.items.len
QtProperty[int] count:
read = getCount
notify = countChanged
proc hasMoreChanged*(self: CollectiblesModel) {.signal.}
proc getHasMore*(self: CollectiblesModel): bool {.slot.} =
self.hasMore
QtProperty[bool] hasMore:
read = getHasMore
notify = hasMoreChanged
proc setHasMore(self: CollectiblesModel, hasMore: bool) =
if hasMore == self.hasMore:
return
self.hasMore = hasMore
self.hasMoreChanged()
method canFetchMore*(self: CollectiblesModel, parent: QModelIndex): bool =
return self.hasMore
proc loadMoreItems(self: CollectiblesModel) {.signal.}
proc loadMore*(self: CollectiblesModel) {.slot.} =
self.loadMoreItems()
method fetchMore*(self: CollectiblesModel, parent: QModelIndex) =
self.loadMore()
method rowCount*(self: CollectiblesModel, index: QModelIndex = nil): int =
return self.getCount()
method roleNames(self: CollectiblesModel): Table[int, string] =
{
CollectibleRole.Uid.int:"uid",
CollectibleRole.ChainId.int:"chainId",
CollectibleRole.ContractAddress.int:"contractAddress",
CollectibleRole.TokenId.int:"tokenId",
CollectibleRole.Name.int:"name",
CollectibleRole.ImageUrl.int:"imageUrl",
}.toTable
method data(self: CollectiblesModel, index: QModelIndex, role: int): QVariant =
if (not index.isValid):
return
if (index.row < 0 or index.row >= self.getCount()):
return
let enumRole = role.CollectibleRole
if index.row < self.items.len:
let item = self.items[index.row]
case enumRole:
of CollectibleRole.Uid:
result = newQVariant(item.getId())
of CollectibleRole.ChainId:
result = newQVariant(item.getChainId())
of CollectibleRole.ContractAddress:
result = newQVariant(item.getContractAddress())
of CollectibleRole.TokenId:
result = newQVariant(item.getTokenId().toString())
of CollectibleRole.Name:
result = newQVariant(item.getName())
of CollectibleRole.ImageUrl:
result = newQVariant(item.getImageUrl())
else:
error "Invalid role for loading item"
result = newQVariant()
proc rowData(self: CollectiblesModel, index: int, column: string): string {.slot.} =
if (index >= self.items.len):
return
let item = self.items[index]
case column:
of "uid": result = item.getId()
of "chainId": result = $item.getChainId()
of "contractAddress": result = item.getContractAddress()
of "tokenId": result = item.getTokenId().toString()
of "name": result = item.getName()
of "imageUrl": result = item.getImageUrl()
proc appendCollectibleItems(self: CollectiblesModel, newItems: seq[CollectibleItem]) =
if len(newItems) == 0:
return
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
# Start after the current last real item
let startIdx = self.items.len
# End at the new last real item
let endIdx = startIdx + newItems.len - 1
self.beginInsertRows(parentModelIndex, startIdx, endIdx)
self.items.insert(newItems, startIdx)
self.endInsertRows()
self.countChanged()
proc removeCollectibleItems(self: CollectiblesModel) =
if self.items.len <= 0:
return
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
# Start from the beginning
let startIdx = 0
# End at the last real item
let endIdx = startIdx + self.items.len - 1
self.beginRemoveRows(parentModelIndex, startIdx, endIdx)
self.items = @[]
self.endRemoveRows()
self.countChanged()
proc getItems*(self: CollectiblesModel): seq[CollectibleItem] =
return self.items
proc setItems*(self: CollectiblesModel, newItems: seq[CollectibleItem], offset: int, hasMore: bool) =
if offset == 0:
self.removeCollectibleItems()
elif offset != self.getCollectiblesCount():
error "invalid offset"
return
self.appendCollectibleItems(newItems)
self.setHasMore(hasMore)
proc getImageUrl*(self: CollectiblesModel, id: string): string {.slot.} =
for item in self.items:
if(cmpIgnoreCase(item.getId(), id) == 0):
return item.getImageUrl()
return ""
proc getName*(self: CollectiblesModel, id: string): string {.slot.} =
for item in self.items:
if(cmpIgnoreCase(item.getId(), id) == 0):
return item.getName()
return ""
proc getActivityToken*(self: CollectiblesModel, id: string): backend_activity.Token =
for item in self.items:
if(cmpIgnoreCase(item.getId(), id) == 0):
result.tokenType = backend_activity.TokenType.Erc721
result.chainId = backend_activity.ChainId(item.getChainId())
var contract = item.getContractAddress()
if len(contract) > 0:
var address: eth.Address
address = eth.fromHex(eth.Address, contract)
result.address = some(address)
var tokenId = item.getTokenId()
if tokenId > 0:
result.tokenId = some(backend_activity.TokenId("0x" & stint.toHex(tokenId)))
return result
# Fallback, use data from id
var parts = id.split("+")
if len(parts) == 3:
result.chainId = backend_activity.ChainId(parseInt(parts[0]))
result.address = some(eth.fromHex(eth.Address, parts[1]))
var tokenIdInt = u256(parseInt(parts[2]))
result.tokenId = some(backend_activity.TokenId("0x" & stint.toHex(tokenIdInt)))
return result
proc getUidForData*(self: CollectiblesModel, tokenId: string, tokenAddress: string, chainId: int): string {.slot.} =
for item in self.items:
if(cmpIgnoreCase(item.getTokenId().toString(), tokenId) == 0 and cmpIgnoreCase(item.getContractAddress(), tokenAddress) == 0):
return item.getId()
# Fallback, create uid from data, because it still might not be fetched
if chainId > 0 and len(tokenAddress) > 0 and len(tokenId) > 0:
return $chainId & "+" & tokenAddress & "+" & tokenId
return ""

View File

@ -5,6 +5,8 @@ import model
import entry
import entry_details
import recipients_model
import collectibles_model
import collectibles_item
import events_handler
import status
@ -29,6 +31,7 @@ proc toRef*[T](obj: T): ref T =
const FETCH_BATCH_COUNT_DEFAULT = 10
const FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT = 2000
const FETCH_COLLECTIBLES_BATCH_COUNT_DEFAULT = 2000
type
CollectiblesToTokenConverter* = proc (id: string): backend_activity.Token
@ -39,6 +42,7 @@ QtObject:
model: Model
recipientsModel: RecipientsModel
collectiblesModel: CollectiblesModel
currentActivityFilter: backend_activity.ActivityFilter
currencyService: currency_service.Service
tokenService: token_service.Service
@ -77,6 +81,12 @@ QtObject:
QtProperty[QVariant] recipientsModel:
read = getRecipientsModel
proc getCollectiblesModel*(self: Controller): QVariant {.slot.} =
return newQVariant(self.collectiblesModel)
QtProperty[QVariant] collectiblesModel:
read = getCollectiblesModel
proc buildMultiTransactionExtraData(self: Controller, metadata: backend_activity.ActivityEntry): ExtraData =
if metadata.symbolIn.isSome():
result.inAmount = self.currencyService.parseCurrencyValue(metadata.symbolIn.get(), metadata.amountIn)
@ -170,6 +180,22 @@ QtObject:
error "error fetching activity entries: ", response.error
return
proc updateCollectiblesModel*(self: Controller) {.slot.} =
self.status.setLoadingCollectibles(true)
let res = backend_activity.getActivityCollectiblesAsync(self.requestId, self.chainIds, self.addresses, 0, FETCH_COLLECTIBLES_BATCH_COUNT_DEFAULT)
if res.error != nil:
self.status.setLoadingCollectibles(false)
error "error fetching collectibles: ", res.error
return
proc loadMoreCollectibles*(self: Controller) {.slot.} =
self.status.setLoadingCollectibles(true)
let res = backend_activity.getActivityCollectiblesAsync(self.requestId, self.chainIds, self.addresses, self.collectiblesModel.getCount(), FETCH_COLLECTIBLES_BATCH_COUNT_DEFAULT)
if res.error != nil:
self.status.setLoadingCollectibles(false)
error "error fetching collectibles: ", res.error
return
proc setFilterTime*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} =
self.currentActivityFilter.period = backend_activity.newPeriod(startTimestamp, endTimestamp)
@ -234,6 +260,21 @@ QtObject:
self.status.setStartTimestamp(res.timestamp)
)
self.eventsHandler.onGetCollectiblesDone(proc (jsonObj: JsonNode) =
defer: self.status.setLoadingCollectibles(false)
let res = fromJson(jsonObj, backend_activity.GetCollectiblesResponse)
if res.errorCode != ErrorCodeSuccess:
error "error fetching collectibles: ", res.errorCode
return
try:
let items = res.collectibles.map(header => collectibleToItem(header))
self.collectiblesModel.setItems(items, res.offset, res.hasMore)
except Exception as e:
error "Error converting activity entries: ", e.msg
)
self.eventsHandler.onNewDataAvailable(proc () =
self.status.setNewDataAvailable(true)
)
@ -248,6 +289,7 @@ QtObject:
result.requestId = requestId
result.model = newModel()
result.recipientsModel = newRecipientsModel()
result.collectiblesModel = newCollectiblesModel()
result.tokenService = tokenService
result.currentActivityFilter = backend_activity.getIncludeAllActivityFilter()
@ -364,6 +406,7 @@ QtObject:
proc setFilterChains*(self: Controller, chainIds: seq[int], allEnabled: bool) =
self.chainIds = chainIds
self.status.setIsFilterDirty(true)
self.status.emitFilterChainsChanged()
self.status.emitFilterChainsChanged()
self.updateAssetsIdentities()

View File

@ -46,6 +46,9 @@ QtObject:
proc onGetOldestTimestampDone*(self: EventsHandler, handler: EventCallbackProc) =
self.eventHandlers[backend_activity.eventActivityGetOldestTimestampDone] = handler
proc onGetCollectiblesDone*(self: EventsHandler, handler: EventCallbackProc) =
self.eventHandlers[backend_activity.eventActivityGetCollectiblesDone] = handler
proc onNewDataAvailable*(self: EventsHandler, handler: proc()) =
self.newDataAvailableFn = handler

View File

@ -13,6 +13,7 @@ QtObject:
errorCode: backend_activity.ErrorCode
loadingRecipients: Atomic[int]
loadingCollectibles: Atomic[int]
loadingStartTimestamp: Atomic[int]
startTimestamp: int
@ -44,6 +45,12 @@ QtObject:
discard fetchAdd(self.loadingRecipients, if loadingData: 1 else: -1)
self.loadingRecipientsChanged()
proc loadingCollectiblesChanged*(self: Status) {.signal.}
proc setLoadingCollectibles*(self: Status, loadingData: bool) =
discard fetchAdd(self.loadingCollectibles, if loadingData: 1 else: -1)
self.loadingCollectiblesChanged()
proc loadingStartTimestampChanged*(self: Status) {.signal.}
proc setLoadingStartTimestamp*(self: Status, loadingData: bool) =

View File

@ -20,6 +20,7 @@ const eventActivityFilteringUpdate*: string = "wallet-activity-filtering-entries
const eventActivityGetRecipientsDone*: string = "wallet-activity-get-recipients-result"
const eventActivityGetOldestTimestampDone*: string = "wallet-activity-get-oldest-timestamp-result"
const eventActivityFetchTransactionDetails*: string = "wallet-activity-fetch-transaction-details-result"
const eventActivityGetCollectiblesDone*: string = "wallet-activity-get-collectibles"
type
Period* = object
@ -527,6 +528,67 @@ rpc(getOldestActivityTimestampAsync, "wallet"):
requestId: int32
addresses: seq[string]
type
# Mirrors services/wallet/thirdparty/collectible_types.go ContractID
ContractID* = ref object of RootObj
chainID*: int
address*: string
# Mirrors services/wallet/thirdparty/collectible_types.go CollectibleUniqueID
CollectibleUniqueID* = ref object of RootObj
contractID*: ContractID
tokenID*: UInt256
# see services/wallet/activity/service.go CollectibleHeader
CollectibleHeader* = object
id* : CollectibleUniqueID
name*: string
imageUrl*: string
# see services/wallet/activity/service.go CollectiblesResponse
GetCollectiblesResponse* = object
collectibles*: seq[CollectibleHeader]
offset*: int
hasMore*: bool
errorCode*: ErrorCode
proc fromJson*(t: JsonNode, T: typedesc[ContractID]): ContractID {.inline.} =
result = ContractID()
result.chainID = t["chainID"].getInt()
result.address = t["address"].getStr()
proc fromJson*(t: JsonNode, T: typedesc[CollectibleUniqueID]): CollectibleUniqueID {.inline.} =
result = CollectibleUniqueID()
result.contractID = fromJson(t["contractID"], ContractID)
result.tokenID = stint.parse(t["tokenID"].getStr(), UInt256)
proc fromJson*(t: JsonNode, T: typedesc[CollectibleHeader]): CollectibleHeader {.inline.} =
result = CollectibleHeader()
result.id = fromJson(t["id"], CollectibleUniqueID)
result.name = t["name"].getStr()
result.imageUrl = t["image_url"].getStr()
proc fromJson*(e: JsonNode, T: typedesc[GetCollectiblesResponse]): GetCollectiblesResponse {.inline.} =
var collectibles: seq[CollectibleHeader] = @[]
if e.hasKey("collectibles"):
let jsonCollectibles = e["collectibles"]
for item in jsonCollectibles.getElems():
collectibles.add(fromJson(item, CollectibleHeader))
result = T(
collectibles: collectibles,
hasMore: e["hasMore"].getBool(),
offset: e["offset"].getInt(),
errorCode: ErrorCode(e["errorCode"].getInt())
)
rpc(getActivityCollectiblesAsync, "wallet"):
requestId: int32
chainIDs: seq[int]
addresses: seq[string]
offset: int
limit: int
rpc(getMultiTxDetails, "wallet"):
id: int

View File

@ -178,11 +178,11 @@ Column {
onClosed: activityFilterStore.toggleCollectibles(uid)
Connections {
// Collectibles model is fetched asynchronousl, so data might not be available
target: activityFilterStore.collectiblesList
// Collectibles model is fetched asynchronously, so data might not be available
target: activityFilterStore
enabled: !collectibleTag.isValid
function onIsFetchingChanged() {
if (activityFilterStore.collectiblesList.isFetching || !activityFilterStore.collectiblesList.hasMore)
function onLoadingCollectiblesChanged() {
if (activityFilterStore.loadingCollectibles || !activityFilterStore.collectiblesList.hasMore)
return
collectibleTag.uid = ""
collectibleTag.uid = modelData
@ -262,6 +262,7 @@ Column {
store: root.store
recentsList: activityFilterStore.recentsList
loadingRecipients: activityFilterStore.loadingRecipients
loadingCollectibles: activityFilterStore.loadingCollectibles
recentsFilters: activityFilterStore.recentsFilters
savedAddressList: activityFilterStore.savedAddressList
savedAddressFilters: activityFilterStore.savedAddressFilters

View File

@ -33,6 +33,7 @@ StatusMenu {
// Collectibles filter
property var collectiblesList: []
property var collectiblesFilter: []
property bool loadingCollectibles: false
readonly property bool allCollectiblesChecked: tokensMenu.allCollectiblesChecked
signal updateCollectiblesFilter(string uid)
@ -96,6 +97,7 @@ StatusMenu {
tokensFilter: root.tokensFilter
collectiblesList: root.collectiblesList
collectiblesFilter: root.collectiblesFilter
loadingCollectibles: root.loadingCollectibles
onTokenToggled: updateTokensFilter(tokenSymbol)
onCollectibleToggled: updateCollectiblesFilter(uid)
closePolicy: root.closePolicy

View File

@ -20,6 +20,7 @@ StatusMenu {
property var tokensList: []
readonly property bool allTokensChecked: tokensFilter.length === 0
property bool loadingCollectibles: false
property var collectiblesList: []
property var collectiblesFilter: []
readonly property bool allCollectiblesChecked: collectiblesFilter.length === 0
@ -37,11 +38,6 @@ StatusMenu {
collectiblesSearchBox.reset()
}
QtObject {
id: d
readonly property bool isFetching: root.collectiblesList.isFetching
}
contentItem: ColumnLayout {
spacing: 12
MenuBackButton {
@ -186,7 +182,7 @@ StatusMenu {
allChecked: root.allCollectiblesChecked
checked: !loading && (root.allCollectiblesChecked || root.collectiblesFilter.includes(model.uid))
onActionTriggered: root.collectibleToggled(model.uid)
loading: d.isFetching
loading: root.loadingCollectibles
}
}
}

View File

@ -193,8 +193,15 @@ QtObject {
}
// Collectibles Filters
property var collectiblesList: walletSection.collectiblesController.model
property var collectiblesList: activityController.collectiblesModel
property var collectiblesFilter: []
property bool loadingCollectibles: activityController.status.loadingCollectibles
function updateCollectiblesModel() {
activityController.updateCollectiblesModel()
}
function loadMoreCollectibles() {
activityController.loadMoreCollectibles()
}
function toggleCollectibles(uid) {
// update filters
collectiblesFilter = d.toggleFilterState(collectiblesFilter, uid, collectiblesList.count)

View File

@ -275,7 +275,7 @@ QtObject {
}
function isTxRepeatable(tx) {
if (tx.txType !== Constants.TransactionType.Send)
if (!tx || tx.txType !== Constants.TransactionType.Send)
return false
let res = root.lookupAddressObject(tx.sender)

View File

@ -12,6 +12,7 @@ import StatusQ.Popups 0.1
import shared.controls 1.0
import shared.panels 1.0
import shared.stores 1.0
import utils 1.0
import shared.popups.send 1.0

View File

@ -48,6 +48,7 @@ ColumnLayout {
WalletStores.RootStore.currentActivityFiltersStore.applyAllFilters()
}
WalletStores.RootStore.currentActivityFiltersStore.updateCollectiblesModel()
WalletStores.RootStore.currentActivityFiltersStore.updateRecipientsModel()
}
@ -58,10 +59,15 @@ ColumnLayout {
RootStore.updateTransactionFilter()
}
function onFilterChainsChanged() {
WalletStores.RootStore.currentActivityFiltersStore.updateCollectiblesModel()
WalletStores.RootStore.currentActivityFiltersStore.updateRecipientsModel()
}
}
Connections {
enabled: root.visible
}
QtObject {
id: d
readonly property bool isInitialLoading: RootStore.loadingHistoryTransactions && transactionListRoot.count === 0

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit cff96f99e0997b3f3197eecf7b568f7c2e42cac1
Subproject commit ecc8b4cb5513ed29deb4fdc85eaeed5e96d1e562