feat(wallet) lazy load activity NFT information

Bump status-go to include required changes

Implement processing of activity updated message

Updates: #11600
This commit is contained in:
Stefan 2023-08-11 18:30:19 +01:00 committed by Stefan Dunca
parent 722a9022e2
commit 2c769602f4
7 changed files with 170 additions and 47 deletions

View File

@ -1,4 +1,4 @@
import NimQml, logging, std/json, sequtils, sugar, options, strutils
import NimQml, logging, std/json, sequtils, sugar, options, strutils, locks
import tables, stint, sets
import model
@ -38,6 +38,7 @@ QtObject:
type
Controller* = ref object of QObject
model: Model
recipientsModel: RecipientsModel
currentActivityFilter: backend_activity.ActivityFilter
currencyService: currency_service.Service
@ -229,6 +230,7 @@ QtObject:
return
let entries = self.backendToPresentation(res.activities)
self.model.setEntries(entries, res.offset, res.hasMore)
if len(entries) > 0:
@ -237,7 +239,9 @@ QtObject:
proc updateFilter*(self: Controller) {.slot.} =
self.status.setLoadingData(true)
self.status.setIsFilterDirty(false)
self.model.resetModel(@[])
self.eventsHandler.updateSubscribedAddresses(self.addresses)
self.eventsHandler.updateSubscribedChainIDs(self.chainIds)
self.status.setNewDataAvailable(false)
@ -250,7 +254,6 @@ QtObject:
proc loadMoreItems(self: Controller) {.slot.} =
self.status.setLoadingData(true)
let response = backend_activity.filterActivityAsync(self.requestId, self.addresses, seq[backend_activity.ChainId](self.chainIds), self.currentActivityFilter, self.model.getCount(), FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil:
self.status.setLoadingData(false)
@ -288,6 +291,17 @@ QtObject:
self.processResponse(jsonObj)
)
self.eventsHandler.onFilteringUpdateDone(proc (jn: JsonNode) =
if jn.kind != JArray:
error "expected an array"
var entries = newSeq[backend_activity.Data](jn.len)
for i in 0 ..< jn.len:
entries[i] = fromJson(jn[i], backend_activity.Data)
self.model.updateEntries(entries)
)
self.eventsHandler.onGetRecipientsDone(proc (jsonObj: JsonNode) =
defer: self.status.setLoadingRecipients(false)
let res = fromJson(jsonObj, backend_activity.GetRecipientsResponse)

View File

@ -28,7 +28,6 @@ type
# It is used to display an activity history entry in the QML UI
#
# TODO remove this legacy after the NFT is served async; see #11598
# TODO add all required metadata from filtering; see #11597
#
# Looking into going away from carying the whole detailed data and just keep the required data for the UI
# and request the detailed data on demand
@ -50,6 +49,9 @@ QtObject:
amountCurrency: CurrencyAmount
noAmount: CurrencyAmount
nftName: string
nftImageURL: string
proc setup(self: ActivityEntry) =
self.QObject.setup
@ -129,6 +131,9 @@ QtObject:
QtProperty[string] id:
read = getId
proc getMetadata*(self: ActivityEntry): backend.ActivityEntry =
return self.metadata
proc getSender*(self: ActivityEntry): string {.slot.} =
return if self.metadata.sender.isSome(): "0x" & self.metadata.sender.unsafeGet().toHex() else: ""
@ -199,21 +204,33 @@ QtObject:
QtProperty[bool] isNFT:
read = getIsNFT
proc getNFTName*(self: ActivityEntry): string {.slot.} =
# TODO: complete this async #11597
return ""
proc nftNameChanged*(self: ActivityEntry) {.signal.}
proc getNftName*(self: ActivityEntry): string {.slot.} =
return self.nftName
proc setNftName*(self: ActivityEntry, nftName: string) =
self.nftName = nftName
self.nftNameChanged()
# TODO: lazy load this in activity history service. See #11597
QtProperty[string] nftName:
read = getNFTName
read = getNftName
write = setNftName
notify = nftNameChanged
proc getNFTImageURL*(self: ActivityEntry): string {.slot.} =
# TODO: complete this async #11597
return ""
proc nftImageUrlChanged*(self: ActivityEntry) {.signal.}
# TODO: lazy load this in activity history service. See #11597
QtProperty[string] nftImageURL:
read = getNFTImageURL
proc getNftImageUrl*(self: ActivityEntry): string {.slot.} =
return self.nftImageUrl
proc setNftImageUrl*(self: ActivityEntry, nftImageUrl: string) =
self.nftImageUrl = nftImageUrl
self.nftImageUrlChanged()
QtProperty[string] nftImageUrl:
read = getNftImageUrl
write = setNftImageUrl
notify = nftImageUrlChanged
proc getTotalFees*(self: ActivityEntry): QVariant {.slot.} =
if self.transaction == nil:

View File

@ -37,6 +37,9 @@ QtObject:
proc onFilteringDone*(self: EventsHandler, handler: EventCallbackProc) =
self.eventHandlers[backend_activity.eventActivityFilteringDone] = handler
proc onFilteringUpdateDone*(self: EventsHandler, handler: EventCallbackProc) =
self.eventHandlers[backend_activity.eventActivityFilteringUpdate] = handler
proc onGetRecipientsDone*(self: EventsHandler, handler: EventCallbackProc) =
self.eventHandlers[backend_activity.eventActivityGetRecipientsDone] = handler
@ -56,13 +59,8 @@ QtObject:
let callback = self.walletEventHandlers[data.eventType]
callback(data)
elif self.eventHandlers.hasKey(data.eventType):
var responseJson: JsonNode
responseJson = parseJson(data.message)
if responseJson.kind != JObject:
error "unexpected json type", responseJson.kind
return
let callback = self.eventHandlers[data.eventType]
let responseJson = parseJson(data.message)
callback(responseJson)
proc setupWalletEventHandlers(self: EventsHandler) =

View File

@ -1,8 +1,10 @@
import NimQml, Tables, strutils, strformat, sequtils, logging
import NimQml, Tables, strutils, strformat, sequtils, logging, options
import ./entry
import app/modules/shared_models/currency_amount
import backend/activity as backend
import backend/backend as importing_transactionidentity_comp
type
ModelRole {.pure.} = enum
@ -11,7 +13,7 @@ type
QtObject:
type
Model* = ref object of QAbstractListModel
entries: seq[ActivityEntry]
entries: seq[entry.ActivityEntry]
hasMore: bool
proc delete(self: Model) =
@ -23,10 +25,12 @@ QtObject:
proc newModel*(): Model =
new(result, delete)
result.entries = @[]
result.setup
result.hasMore = true
result.setup
proc `$`*(self: Model): string =
for i in 0 ..< self.entries.len:
result &= fmt"""[{i}]:({$self.entries[i]})"""
@ -68,12 +72,12 @@ QtObject:
self.hasMore = hasMore
self.hasMoreChanged()
proc resetModel*(self: Model, newEntries: seq[ActivityEntry]) =
proc resetModel*(self: Model, newEntries: seq[entry.ActivityEntry]) =
self.beginResetModel()
self.entries = newEntries
self.endResetModel()
proc setEntries*(self: Model, newEntries: seq[ActivityEntry], offset: int, hasMore: bool) =
proc setEntries*(self: Model, newEntries: seq[entry.ActivityEntry], offset: int, hasMore: bool) =
if offset == 0:
self.resetModel(newEntries)
else:
@ -83,12 +87,34 @@ QtObject:
if offset != self.entries.len:
error "offset != self.entries.len"
return
self.beginInsertRows(parentModelIndex, self.entries.len, self.entries.len + newEntries.len - 1)
self.entries.add(newEntries)
self.endInsertRows()
self.countChanged()
self.setHasMore(hasMore)
proc sameIdentity(e: entry.ActivityEntry, d: backend.Data): bool =
let m = e.getMetadata()
if m.payloadType != d.payloadType:
return false
if m.payloadType == MultiTransaction:
return m.id == d.id.get()
return m.transaction.isSome() and d.transaction.isSome() and m.transaction.get() == d.transaction.get()
proc updateEntries*(self: Model, updates: seq[backend.Data]) =
for i in countdown(self.entries.high, 0):
for j in countdown(updates.high, 0):
if sameIdentity(self.entries[i], updates[j]):
if updates[j].nftName.isSome():
self.entries[i].setNftName(updates[j].nftName.get())
if updates[j].nftUrl.isSome():
self.entries[i].setNftImageUrl(updates[j].nftUrl.get())
break
proc getHasMore*(self: Model): bool {.slot.} =
return self.hasMore

View File

@ -16,6 +16,7 @@ const noLimitTimestampForPeriod* = 0
# Declared in services/wallet/activity/service.go
const eventActivityFilteringDone*: string = "wallet-activity-filtering-done"
const eventActivityFilteringUpdate*: string = "wallet-activity-filtering-entries-updated"
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"
@ -100,8 +101,6 @@ proc `$`*(tt: TokenType): string {.inline.} =
return "ERC-721"
of Erc1155:
return "ERC-1155"
else:
return ""
proc fromJson*(jn: JsonNode, T: typedesc[TokenType]): TokenType {.inline.} =
return cast[TokenType](jn.getInt())
@ -211,11 +210,9 @@ type
SimpleTransaction
PendingTransaction
# Define toJson proc for PayloadType
proc `%`*(pt: PayloadType): JsonNode {.inline.} =
return newJInt(ord(pt))
# Define fromJson proc for PayloadType
proc fromJson*(jn: JsonNode, T: typedesc[PayloadType]): PayloadType {.inline.} =
return cast[PayloadType](jn.getInt())
@ -250,11 +247,9 @@ type
Erc721
Erc1155
# Define toJson proc for TransferType
proc `%`*(pt: TransferType): JsonNode {.inline.} =
return newJInt(ord(pt))
# Define fromJson proc for TransferType
proc fromJson*(jn: JsonNode, T: typedesc[TransferType]): TransferType {.inline.} =
return cast[TransferType](jn.getInt())
@ -284,6 +279,32 @@ type
chainIdIn*: Option[ChainId]
transferType*: Option[TransferType]
# Mirrors status-go/services/wallet/activity/activity.go EntryData
Data* = object
payloadType*: PayloadType
transaction*: Option[TransactionIdentity]
id*: Option[int]
timestamp*: Option[int]
activityType*: Option[ActivityType]
activityStatus*: Option[ActivityStatus]
amountOut*: Option[UInt256]
amountIn*: Option[UInt256]
tokenOut*: Option[Token]
tokenIn*: Option[Token]
sender*: Option[eth.Address]
recipient*: Option[eth.Address]
chainIdOut*: Option[ChainId]
chainIdIn*: Option[ChainId]
transferType*: Option[TransferType]
nftName*: Option[string]
nftUrl*: Option[string]
# Mirrors services/wallet/activity/service.go ErrorCode
ErrorCode* = enum
ErrorCodeSuccess = 1,
@ -297,12 +318,17 @@ type
hasMore*: bool
errorCode*: ErrorCode
# Define toJson proc for PayloadType
proc toJson*(ae: ActivityEntry): JsonNode {.inline.} =
return %*(ae)
# Define fromJson proc for PayloadType
proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.} =
proc fromJson*(e: JsonNode, T: typedesc[Data]): Data {.inline.} =
const transactionField = "transaction"
const idField = "id"
const activityTypeField = "activityType"
const activityStatusField = "activityStatus"
const timestampField = "timestamp"
const amountOutField = "amountOut"
const amountInField = "amountIn"
const tokenOutField = "tokenOut"
const tokenInField = "tokenIn"
const senderField = "sender"
@ -310,20 +336,26 @@ proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.
const chainIdOutField = "chainIdOut"
const chainIdInField = "chainIdIn"
const transferTypeField = "transferType"
const nftNameField = "nftName"
const nftUrlField = "nftUrl"
result = T(
payloadType: fromJson(e["payloadType"], PayloadType),
transaction: if e.hasKey("transaction"):
fromJson(e["transaction"], Option[TransactionIdentity])
transaction: if e.hasKey(transactionField):
fromJson(e[transactionField], Option[TransactionIdentity])
else:
none(TransactionIdentity),
id: e["id"].getInt(),
activityType: fromJson(e["activityType"], ActivityType),
activityStatus: fromJson(e["activityStatus"], ActivityStatus),
timestamp: e["timestamp"].getInt(),
amountOut: stint.fromHex(UInt256, e["amountOut"].getStr()),
amountIn: stint.fromHex(UInt256, e["amountIn"].getStr()),
id: if e.hasKey(idField): some(e[idField].getInt()) else: none(int),
activityType: if e.hasKey(activityTypeField):
some(fromJson(e[activityTypeField], ActivityType))
else:
none(ActivityType),
activityStatus: if e.hasKey(activityStatusField):
some(fromJson(e[activityStatusField], ActivityStatus))
else:
none(ActivityStatus),
timestamp: if e.hasKey(timestampField): some(e[timestampField].getInt()) else: none(int),
amountOut: if e.hasKey(amountOutField): some(stint.fromHex(UInt256, e[amountOutField].getStr())) else: none(UInt256),
amountIn: if e.hasKey(amountInField): some(stint.fromHex(UInt256, e[amountInField].getStr())) else: none(UInt256),
tokenOut: if e.contains(tokenOutField):
some(fromJson(e[tokenOutField], Token))
else:
@ -332,6 +364,9 @@ proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.
some(fromJson(e[tokenInField], Token))
else:
none(Token),
nftName: if e.contains(nftNameField): some(e[nftNameField].getStr()) else: none(string),
nftUrl: if e.contains(nftUrlField): some(e[nftUrlField].getStr()) else: none(string),
)
if e.hasKey(senderField) and e[senderField].kind != JNull:
var address: eth.Address
@ -348,6 +383,26 @@ proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.
if e.hasKey(transferTypeField) and e[transferTypeField].kind != JNull:
result.transferType = some(fromJson(e[transferTypeField], TransferType))
proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.} =
let data = fromJson(e, Data)
result = T(
payloadType: data.payloadType,
transaction: data.transaction,
id: if data.id.isSome: data.id.get() else: 0,
activityType: data.activityType.get(),
activityStatus: data.activityStatus.get(),
timestamp: data.timestamp.get(),
amountOut: data.amountOut.get(),
amountIn: data.amountIn.get(),
tokenOut: data.tokenOut,
tokenIn: data.tokenIn,
sender: data.sender,
recipient: data.recipient,
chainIdOut: data.chainIdOut,
chainIdIn: data.chainIdIn,
transferType: data.transferType,
)
proc `$`*(self: ActivityEntry): string =
let transactionStr = if self.transaction.isSome: $self.transaction.get()
else: "none(TransactionIdentity)"

View File

@ -1,4 +1,4 @@
import json, stint, json_serialization
import json, stint, json_serialization, strformat
import ../app_service/service/eth/dto/transaction
import ./core as core
@ -41,6 +41,19 @@ const EventNonArchivalNodeDetected*: string = "non-archival-node-detected"
const EventPendingTransactionUpdate*: string = "pending-transaction-update"
const EventMTTransactionUpdate*: string = "multi-transaction-update"
proc `$`*(self: MultiTransactionDto): string =
return fmt"""MultiTransactionDto(
id:{self.id},
timestamp:{self.timestamp},
fromAddress:{self.fromAddress},
toAddress:{self.toAddress},
fromAsset:{self.fromAsset},
toAsset:{self.toAsset},
fromAmount:{self.fromAmount},
toAmount:{self.toAmount},
multiTxType:{self.multiTxType}
)"""
proc getTransactionByHash*(chainId: int, hash: string): RpcResponse[JsonNode] {.raises: [Exception].} =
core.callPrivateRPCWithChainId("eth_getTransactionByHash", chainId, %* [hash])

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 71800a19f1ee636652a208e77c550f72e9b8863d
Subproject commit c0f32748b4927b5e3272e1e8e9cb6b226f002720