feat(activity): add incremental updates to current activity filter

Switch the activity filter to use the new session-based API that
deliver incremental updates to the current filter.

Drop the old quick win listening for individual change events and
use the unified API instead.

The new transactions (on-top) trigger the old "new transactions" buttons
that trigger reset of the current filter and the top new transacitons
highlighted.

Highlight mixed changes (not new on top) as they come in

Highlight new changes on filter reset

Closes #12120
This commit is contained in:
Stefan 2024-02-08 09:49:12 -03:00 committed by Stefan Dunca
parent 115610fcf6
commit 9202cce3f5
17 changed files with 429 additions and 223 deletions

View File

@ -63,8 +63,6 @@ QtObject:
# call updateAssetsIdentities after updating chainIds
chainIds: seq[int]
requestId: int32
proc setup(self: Controller) =
self.QObject.setup
@ -89,31 +87,9 @@ QtObject:
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)
if metadata.symbolOut.isSome():
result.outAmount = self.currencyService.parseCurrencyValue(metadata.symbolOut.get(), metadata.amountOut)
proc buildTransactionExtraData(self: Controller, metadata: backend_activity.ActivityEntry): ExtraData =
if metadata.symbolIn.isSome() or metadata.amountIn > 0:
result.inAmount = self.currencyService.parseCurrencyValue(metadata.symbolIn.get(""), metadata.amountIn)
if metadata.symbolOut.isSome() or metadata.amountOut > 0:
result.outAmount = self.currencyService.parseCurrencyValue(metadata.symbolOut.get(""), metadata.amountOut)
proc backendToPresentation(self: Controller, backendEntities: seq[backend_activity.ActivityEntry]): seq[entry.ActivityEntry] =
let amountToCurrencyConvertor = proc(amount: UInt256, symbol: string): CurrencyAmount =
return currencyAmountToItem(self.currencyService.parseCurrencyValue(symbol, amount),
self.currencyService.getCurrencyFormat(symbol))
for backendEntry in backendEntities:
var ae: entry.ActivityEntry
case backendEntry.getPayloadType():
of MultiTransaction:
let extraData = self.buildMultiTransactionExtraData(backendEntry)
ae = entry.newMultiTransactionActivityEntry(backendEntry, extraData, amountToCurrencyConvertor)
of SimpleTransaction, PendingTransaction:
let extraData = self.buildTransactionExtraData(backendEntry)
ae = entry.newTransactionActivityEntry(backendEntry, self.addresses, extraData, amountToCurrencyConvertor)
let ae = entry.newActivityEntry(backendEntry, self.addresses, self.currencyService)
result.add(ae)
proc fetchTxDetails*(self: Controller, txID: string) {.slot.} =
@ -143,37 +119,69 @@ QtObject:
self.model.setEntries(entries, res.offset, res.hasMore)
if len(entries) > 0:
self.eventsHandler.updateRelevantTimestamp(entries[len(entries) - 1].getTimestamp())
if res.offset == 0:
self.status.setNewDataAvailable(false)
proc updateFilter*(self: Controller) {.slot.} =
proc sessionId(self: Controller): int32 =
return self.eventsHandler.getSessionId()
proc invalidateData(self: Controller) =
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)
let response = backend_activity.filterActivityAsync(self.requestId, self.addresses, self.allAddressesSelected, seq[backend_activity.ChainId](self.chainIds), self.currentActivityFilter, 0, FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil:
error "error fetching activity entries: ", response.error
# Stops the old session and starts a new one. All the incremental changes are lost
proc newFilterSession*(self: Controller) {.slot.} =
self.invalidateData()
# stop the previous filter session
if self.eventsHandler.hasSessionId():
let res = backend_activity.stopActivityFilterSession(self.sessionId())
if res.error != nil:
error "error stopping the previous session of activity fitlering: ", res.error
self.eventsHandler.clearSessionId()
# start a new filter session
let (sessionId, ok) = backend_activity.newActivityFilterSession(self.addresses, self.allAddressesSelected, seq[backend_activity.ChainId](self.chainIds), self.currentActivityFilter, FETCH_BATCH_COUNT_DEFAULT)
if not ok:
self.status.setLoadingData(false)
return
self.eventsHandler.setSessionId(sessionId)
proc updateFilter*(self: Controller) {.slot.} =
self.invalidateData()
if not backend_activity.updateFilterForSession(self.sessionId(), self.currentActivityFilter, FETCH_BATCH_COUNT_DEFAULT):
self.status.setLoadingData(false)
error "error updating activity filter"
return
proc resetActivityData*(self: Controller) {.slot.} =
self.invalidateData()
let response = backend_activity.resetActivityFilterSession(self.sessionId(), FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil:
self.status.setLoadingData(false)
error "error fetching activity entries from start: ", response.error
return
proc loadMoreItems(self: Controller) {.slot.} =
self.status.setLoadingData(true)
let response = backend_activity.filterActivityAsync(self.requestId, self.addresses, self.allAddressesSelected, seq[backend_activity.ChainId](self.chainIds), self.currentActivityFilter, self.model.getCount(), FETCH_BATCH_COUNT_DEFAULT)
let response = backend_activity.getMoreForActivityFilterSession(self.sessionId(), FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil:
self.status.setLoadingData(false)
error "error fetching activity entries: ", response.error
error "error fetching more 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)
let res = backend_activity.getActivityCollectiblesAsync(self.sessionId(), self.chainIds, self.addresses, 0, FETCH_COLLECTIBLES_BATCH_COUNT_DEFAULT)
if res.error != nil:
self.status.setLoadingCollectibles(false)
error "error fetching collectibles: ", res.error
@ -181,7 +189,7 @@ QtObject:
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)
let res = backend_activity.getActivityCollectiblesAsync(self.sessionId(), 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
@ -210,7 +218,7 @@ QtObject:
proc updateStartTimestamp*(self: Controller) {.slot.} =
self.status.setLoadingStartTimestamp(true)
let resJson = backend_activity.getOldestActivityTimestampAsync(self.requestId, self.addresses)
let resJson = backend_activity.getOldestActivityTimestampAsync(self.sessionId(), self.addresses)
if resJson.error != nil:
self.status.setLoadingStartTimestamp(false)
error "error requesting oldest activity timestamp: ", resJson.error
@ -254,6 +262,24 @@ QtObject:
self.model.updateEntries(entries)
)
self.eventsHandler.onFilteringSessionUpdated(proc (jn: JsonNode) =
if jn.kind != JObject:
error "expected an object"
let res = fromJson(jn, backend_activity.SessionUpdate)
var updated = newSeq[backend_activity.ActivityEntry](len(res.`new`))
var indices = newSeq[int](len(res.`new`))
for i in 0 ..< len(res.`new`):
updated[i] = res.`new`[i].entry
indices[i] = res.`new`[i].pos
self.status.setNewDataAvailable(res.hasNewOnTop)
if len(res.`new`) > 0:
let entries = self.backendToPresentation(updated)
self.model.addNewEntries(entries, indices)
)
self.eventsHandler.onGetRecipientsDone(proc (jsonObj: JsonNode) =
defer: self.status.setLoadingRecipients(false)
let res = fromJson(jsonObj, backend_activity.GetRecipientsResponse)
@ -291,19 +317,13 @@ QtObject:
error "Error converting activity entries: ", e.msg
)
self.eventsHandler.onNewDataAvailable(proc () =
self.status.setNewDataAvailable(true)
)
proc newController*(requestId: int32,
detailsController: details_controller.Controller,
proc newController*(detailsController: details_controller.Controller,
currencyService: currency_service.Service,
tokenService: token_service.Service,
savedAddressService: saved_address_service.Service,
events: EventEmitter): Controller =
new(result, delete)
result.requestId = requestId
result.model = newModel()
result.recipientsModel = newRecipientsModel()
result.collectiblesModel = newCollectiblesModel()
@ -311,7 +331,7 @@ QtObject:
result.savedAddressService = savedAddressService
result.currentActivityFilter = backend_activity.getIncludeAllActivityFilter()
result.eventsHandler = newEventsHandler(result.requestId, events)
result.eventsHandler = newEventsHandler(events)
result.status = newStatus()
result.currencyService = currencyService
@ -452,7 +472,7 @@ QtObject:
proc updateRecipientsModel*(self: Controller) {.slot.} =
self.status.setLoadingRecipients(true)
let res = backend_activity.getRecipientsAsync(self.requestId, self.chainIds, self.addresses, 0, FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT)
let res = backend_activity.getRecipientsAsync(self.sessionId(), self.chainIds, self.addresses, 0, FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT)
if res.error != nil or res.result.kind != JBool:
self.status.setLoadingRecipients(false)
error "error fetching recipients: ", res.error, "; kind ", res.result.kind
@ -464,7 +484,7 @@ QtObject:
proc loadMoreRecipients(self: Controller) {.slot.} =
self.status.setLoadingRecipients(true)
let res = backend_activity.getRecipientsAsync(self.requestId, self.chainIds, self.addresses, self.recipientsModel.getCount(), FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT)
let res = backend_activity.getRecipientsAsync(self.sessionId(), self.chainIds, self.addresses, self.recipientsModel.getCount(), FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT)
if res.error != nil:
self.status.setLoadingRecipients(false)
error "error fetching more recipient entries: ", res.error
@ -483,8 +503,12 @@ QtObject:
proc globalFilterChanged*(self: Controller, addresses: seq[string], allAddressesSelected: bool, chainIds: seq[int], allChainsEnabled: bool) =
if (self.addresses == addresses and self.allAddressesSelected == allAddressesSelected and self.chainIds == chainIds):
return
self.setFilterAddresses(addresses, allAddressesSelected)
self.setFilterChains(chainIds, allChainsEnabled)
# Every change of chains and addresses have to start a new session to get incremental updates when filter is cleared
self.newFilterSession()
proc noLimitTimestamp*(self: Controller): int {.slot.} =
return backend_activity.noLimitTimestampForPeriod

View File

@ -7,6 +7,8 @@ import app/global/global_singleton
import app_service/service/currency/service
import app/modules/shared/wallet_utils
import web3/ethtypes as eth
# Additional data needed to build an Entry, which is
@ -17,13 +19,10 @@ type
inAmount*: float64
outAmount*: float64
AmountToCurrencyConvertor* = proc (amount: UInt256, symbol: string): CurrencyAmount
# Used to display an activity history header entry in the QML UI
QtObject:
type
ActivityEntry* = ref object of QObject
valueConvertor: AmountToCurrencyConvertor
metadata: backend.ActivityEntry
extradata: ExtraData
@ -33,6 +32,9 @@ QtObject:
nftName: string
nftImageURL: string
# true for entries that were changed/added in the current session
highlight: bool
proc setup(self: ActivityEntry) =
self.QObject.setup
@ -42,32 +44,61 @@ QtObject:
proc isInTransactionType(self: ActivityEntry): bool =
return self.metadata.activityType == backend.ActivityType.Receive or self.metadata.activityType == backend.ActivityType.Mint
proc newMultiTransactionActivityEntry*(metadata: backend.ActivityEntry, extradata: ExtraData, valueConvertor: AmountToCurrencyConvertor): ActivityEntry =
proc extractCurrencyAmount(self: ActivityEntry, currencyService: Service): CurrencyAmount =
let amount = if self.isInTransactionType(): self.metadata.amountIn else: self.metadata.amountOut
let symbol = if self.isInTransactionType(): self.metadata.symbolIn.get("") else: self.metadata.symbolOut.get("")
return currencyAmountToItem(
currencyService.parseCurrencyValue(symbol, amount),
currencyService.getCurrencyFormat(symbol),
)
proc newMultiTransactionActivityEntry*(metadata: backend.ActivityEntry, extradata: ExtraData, currencyService: Service): ActivityEntry =
new(result, delete)
result.valueConvertor = valueConvertor
result.metadata = metadata
result.extradata = extradata
result.noAmount = newCurrencyAmount()
result.amountCurrency = valueConvertor(
if result.isInTransactionType(): metadata.amountIn else: metadata.amountOut,
if result.isInTransactionType(): metadata.symbolIn.get("") else: metadata.symbolOut.get(""),
)
result.amountCurrency = result.extractCurrencyAmount(currencyService)
result.highlight = metadata.isNew
result.setup()
proc newTransactionActivityEntry*(metadata: backend.ActivityEntry, fromAddresses: seq[string], extradata: ExtraData, valueConvertor: AmountToCurrencyConvertor): ActivityEntry =
proc newTransactionActivityEntry*(metadata: backend.ActivityEntry, fromAddresses: seq[string], extradata: ExtraData, currencyService: Service): ActivityEntry =
new(result, delete)
result.valueConvertor = valueConvertor
result.metadata = metadata
result.extradata = extradata
result.amountCurrency = valueConvertor(
if result.isInTransactionType(): metadata.amountIn else: metadata.amountOut,
if result.isInTransactionType(): metadata.symbolIn.get("") else: metadata.symbolOut.get(""),
)
result.noAmount = newCurrencyAmount()
result.amountCurrency = result.extractCurrencyAmount(currencyService)
result.highlight = metadata.isNew
result.setup()
proc buildMultiTransactionExtraData(metadata: backend.ActivityEntry, currencyService: Service): ExtraData =
if metadata.symbolIn.isSome():
result.inAmount = currencyService.parseCurrencyValue(metadata.symbolIn.get(), metadata.amountIn)
if metadata.symbolOut.isSome():
result.outAmount = currencyService.parseCurrencyValue(metadata.symbolOut.get(), metadata.amountOut)
proc buildTransactionExtraData(metadata: backend.ActivityEntry, currencyService: Service): ExtraData =
if metadata.symbolIn.isSome() or metadata.amountIn > 0:
result.inAmount = currencyService.parseCurrencyValue(metadata.symbolIn.get(""), metadata.amountIn)
if metadata.symbolOut.isSome() or metadata.amountOut > 0:
result.outAmount = currencyService.parseCurrencyValue(metadata.symbolOut.get(""), metadata.amountOut)
proc newActivityEntry*(backendEntry: backend.ActivityEntry, addresses: seq[string], currencyService: Service): ActivityEntry =
var ae: entry.ActivityEntry
case backendEntry.getPayloadType():
of MultiTransaction:
let extraData = buildMultiTransactionExtraData(backendEntry, currencyService)
ae = newMultiTransactionActivityEntry(backendEntry, extraData, currencyService)
of SimpleTransaction, PendingTransaction:
let extraData = buildTransactionExtraData(backendEntry, currencyService)
ae = newTransactionActivityEntry(backendEntry, addresses, extraData, currencyService)
return ae
proc isMultiTransaction*(self: ActivityEntry): bool {.slot.} =
return self.metadata.getPayloadType() == backend.PayloadType.MultiTransaction
@ -278,6 +309,20 @@ QtObject:
if self.metadata.communityId.isSome():
return self.metadata.communityId.unsafeGet()
return ""
QtProperty[string] communityId:
read = getCommunityId
read = getCommunityId
proc highlightChanged*(self: ActivityEntry) {.signal.}
proc getHighlight*(self: ActivityEntry): bool {.slot.} =
return self.highlight
proc doneHighlighting*(self: ActivityEntry) {.slot.} =
if self.highlight:
self.highlight = false
self.highlightChanged()
QtProperty[bool] highlight:
read = getHighlight
notify = highlightChanged

View File

@ -20,13 +20,7 @@ QtObject:
eventHandlers: Table[string, EventCallbackProc]
walletEventHandlers: Table[string, WalletEventCallbackProc]
# Ignore events older than this relevantTimestamp
relevantTimestamp: int
subscribedAddresses: HashSet[string]
subscribedChainIDs: HashSet[int]
newDataAvailableFn: proc()
requestId: int
sessionId: Option[int32]
proc setup(self: EventsHandler) =
self.QObject.setup
@ -40,6 +34,9 @@ QtObject:
proc onFilteringUpdateDone*(self: EventsHandler, handler: EventCallbackProc) =
self.eventHandlers[backend_activity.eventActivityFilteringUpdate] = handler
proc onFilteringSessionUpdated*(self: EventsHandler, handler: EventCallbackProc) =
self.eventHandlers[backend_activity.eventActivitySessionUpdated] = handler
proc onGetRecipientsDone*(self: EventsHandler, handler: EventCallbackProc) =
self.eventHandlers[backend_activity.eventActivityGetRecipientsDone] = handler
@ -49,13 +46,10 @@ QtObject:
proc onGetCollectiblesDone*(self: EventsHandler, handler: EventCallbackProc) =
self.eventHandlers[backend_activity.eventActivityGetCollectiblesDone] = handler
proc onNewDataAvailable*(self: EventsHandler, handler: proc()) =
self.newDataAvailableFn = handler
proc handleApiEvents(self: EventsHandler, e: Args) =
var data = WalletSignal(e)
if data.requestId.isSome and data.requestId.get() != self.requestId:
if not data.requestId.isSome() or not self.sessionId.isSome() or data.requestId.get() != self.sessionId.get():
return
if self.walletEventHandlers.hasKey(data.eventType):
@ -66,71 +60,29 @@ QtObject:
let responseJson = parseJson(data.message)
callback(responseJson)
proc setupWalletEventHandlers(self: EventsHandler) =
let newDataAvailableCallback = proc (data: WalletSignal) =
if self.newDataAvailableFn == nil:
return
if data.at > 0 and self.relevantTimestamp > 0 and data.at < self.relevantTimestamp:
return
# Check chain, if any was reported
if len(self.subscribedChainIDs) > 0 and data.chainID > 0:
var contains = false
for chainID in self.subscribedChainIDs:
if data.chainID == chainID:
contains = true
break
if not contains:
return
var contains = data.accounts.len == 0
# Check addresses if any was reported
for address in data.accounts:
if address in self.subscribedAddresses:
contains = true
break
if not contains:
return
self.newDataAvailableFn()
# TODO #12120: Replace these specific events with incremental updates events
self.walletEventHandlers[EventNewTransfers] = newDataAvailableCallback
self.walletEventHandlers[EventPendingTransactionUpdate] = newDataAvailableCallback
self.walletEventHandlers[EventMTTransactionUpdate] = newDataAvailableCallback
proc newEventsHandler*(requestId: int, events: EventEmitter): EventsHandler =
proc newEventsHandler*(events: EventEmitter): EventsHandler =
new(result, delete)
result.events = events
result.eventHandlers = initTable[string, EventCallbackProc]()
result.subscribedAddresses = initHashSet[string]()
result.subscribedChainIDs = initHashSet[int]()
result.requestId = requestId
result.setup()
result.setupWalletEventHandlers()
# Register for wallet events
let eventsHandler = result
result.events.on(SignalType.Wallet.event, proc(e: Args) =
eventsHandler.handleApiEvents(e)
)
proc updateRelevantTimestamp*(self: EventsHandler, timestamp: int) =
self.relevantTimestamp = timestamp
proc getSessionId*(self: EventsHandler): int32 =
self.sessionId.get(-1)
proc updateSubscribedAddresses*(self: EventsHandler, addresses: seq[string]) =
self.subscribedAddresses.clear()
for address in addresses:
self.subscribedAddresses.incl(address)
proc setSessionId*(self: EventsHandler, sessionId: int32) =
self.sessionId = some(sessionId)
proc hasSessionId*(self: EventsHandler): bool =
self.sessionId.isSome()
proc clearSessionId*(self: EventsHandler) =
self.sessionId = none(int32)
proc updateSubscribedChainIDs*(self: EventsHandler, chainIDs: seq[int]) =
self.subscribedChainIDs.clear()
for chainID in chainIDs:
self.subscribedChainIDs.incl(chainID)

View File

@ -111,6 +111,19 @@ QtObject:
return m.getTransactionIdentity().isSome() and d.transaction.isSome() and m.getTransactionIdentity().get() == d.transaction.get()
proc addNewEntries*(self: Model, newEntries: seq[entry.ActivityEntry], insertPositions: seq[int]) =
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
for j in countdown(newEntries.high, 0):
let ae = newEntries[j]
let pos = insertPositions[j]
self.beginInsertRows(parentModelIndex, pos, pos)
self.entries.insert(ae, pos)
self.endInsertRows()
self.countChanged()
proc updateEntries*(self: Model, updates: seq[backend.Data]) =
for i in countdown(self.entries.high, 0):
for j in countdown(updates.high, 0):

View File

@ -9,7 +9,7 @@ import backend/activity as backend_activity
QtObject:
type
Status* = ref object of QObject
loadingData: Atomic[int]
loadingData: bool
errorCode: backend_activity.ErrorCode
loadingRecipients: Atomic[int]
@ -36,7 +36,7 @@ QtObject:
proc loadingDataChanged*(self: Status) {.signal.}
proc setLoadingData*(self: Status, loadingData: bool) =
discard fetchAdd(self.loadingData, if loadingData: 1 else: -1)
self.loadingData = loadingData
self.loadingDataChanged()
proc loadingRecipientsChanged*(self: Status) {.signal.}
@ -72,7 +72,7 @@ QtObject:
result.setup()
proc getLoadingData*(self: Status): bool {.slot.} =
return load(self.loadingData) > 0
return self.loadingData
QtProperty[bool] loadingData:
read = getLoadingData

View File

@ -50,11 +50,6 @@ logScope:
export io_interface
type
ActivityID = enum
History
Temporary0
Temporary1
Module* = ref object of io_interface.AccessInterface
delegate: delegate_interface.AccessInterface
events: EventEmitter
@ -145,28 +140,26 @@ proc newModule*(
result.transactionService = transactionService
result.activityDetailsController = activity_detailsc.newController(currencyService)
result.activityController = activityc.newController(
int32(ActivityID.History),
result.activityDetailsController,
result.activityDetailsController,
currencyService,
tokenService,
savedAddressService,
savedAddressService,
events)
result.tmpActivityControllers = [
activityc.newController(
int32(ActivityID.Temporary0),
result.activityDetailsController,
currencyService,
tokenService,
savedAddressService,
events),
activityc.newController(
int32(ActivityID.Temporary1),
result.activityDetailsController,
currencyService,
tokenService,
savedAddressService,
events)
]
result.collectibleDetailsController = collectible_detailsc.newController(int32(backend_collectibles.CollectiblesRequestID.WalletAccount), networkService, events)
result.filter = initFilter(result.controller)

View File

@ -23,6 +23,8 @@ const eventActivityGetOldestTimestampDone*: string = "wallet-activity-get-oldest
const eventActivityFetchTransactionDetails*: string = "wallet-activity-fetch-transaction-details-result"
const eventActivityGetCollectiblesDone*: string = "wallet-activity-get-collectibles"
const eventActivitySessionUpdated*: string = "wallet-activity-session-updated"
type
Period* = object
startTimestamp*: int
@ -62,6 +64,27 @@ type
filterOutAssets*: bool
filterOutCollectibles*: bool
proc `$`*(p: Period): string =
if p.startTimestamp == noLimitTimestampForPeriod and p.endTimestamp == noLimitTimestampForPeriod:
return "Period(UNLIMITED)"
return fmt"""Period(
startTimestamp: {p.startTimestamp},
endTimestamp: {p.endTimestamp}
)"""
proc `$`*(t: ActivityFilter): string =
return fmt"""ActivityFilter(
period: {t.period},
types: {t.types},
statuses: {t.statuses},
counterpartyAddresses: {t.counterpartyAddresses},
assets: {t.assets},
collectibles: {t.collectibles},
filterOutAssets: {t.filterOutAssets},
filterOutCollectibles: {t.filterOutCollectibles}
)"""
proc toJson[T](obj: Option[T]): JsonNode =
if obj.isSome:
toJson(obj.get())
@ -74,6 +97,17 @@ proc fromJson[T](jsonObj: JsonNode, TT: typedesc[Option[T]]): Option[T] =
else:
return none(T)
proc fromJson[T](jsonObj: JsonNode, TT: typedesc[seq[T]]): seq[T] =
if jsonObj.kind != JArray:
error "Expected array, got: ", jsonObj.kind
return @[]
result = newSeq[T](jsonObj.len)
for i, elem in jsonObj.getElems():
result[i] = fromJson(elem, T)
return result
proc `%`*(at: ActivityType): JsonNode {.inline.} =
return newJInt(ord(at))
@ -241,8 +275,6 @@ proc `$`*(pt: ProtocolType): string {.inline.} =
return "Hop"
of Uniswap:
return "Uniswap"
else:
return ""
# Mirrors status-go/services/wallet/activity/activity.go TransferType
type
@ -286,6 +318,7 @@ type
transferType*: Option[TransferType]
communityId*: Option[string]
isNew*: bool
# Mirrors status-go/services/wallet/activity/activity.go EntryData
Data* = object
@ -312,6 +345,8 @@ type
chainIdIn*: Option[ChainId]
transferType*: Option[TransferType]
isNew*: bool
nftName*: Option[string]
nftUrl*: Option[string]
@ -330,6 +365,18 @@ type
hasMore*: bool
errorCode*: ErrorCode
# Mirrors services/wallet/activity/session.go EntryUpdate
EntryUpdate* = object
pos*: int
entry*: ActivityEntry
# Mirrors services/wallet/activity/session.go SessionUpdate
SessionUpdate* = object
hasNewOnTop*: bool
`new`*: seq[EntryUpdate]
removed*: seq[TransactionIdentity]
proc getPayloadType*(ae: ActivityEntry): PayloadType =
return ae.payloadType
@ -366,6 +413,7 @@ proc fromJson*(e: JsonNode, T: typedesc[Data]): Data {.inline.} =
const nftNameField = "nftName"
const nftUrlField = "nftUrl"
const communityIdField = "communityId"
const isNewField = "isNew"
result = T(
payloadType: fromJson(e["payloadType"], PayloadType),
transaction: if e.hasKey(transactionField):
@ -419,9 +467,11 @@ proc fromJson*(e: JsonNode, T: typedesc[Data]): Data {.inline.} =
result.chainIdIn = some(fromJson(e[chainIdInField], ChainId))
if e.hasKey(transferTypeField) and e[transferTypeField].kind != JNull:
result.transferType = some(fromJson(e[transferTypeField], TransferType))
result.isNew = e.hasKey(isNewField) and e[isNewField].getBool()
proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.} =
let data = fromJson(e, Data)
let zeroValue: UInt256 = "0x0".parse(UInt256, 16)
result = T(
payloadType: data.payloadType,
transaction: data.transaction,
@ -429,8 +479,8 @@ proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.
activityType: data.activityType.get(),
activityStatus: data.activityStatus.get(),
timestamp: data.timestamp.get(),
amountOut: data.amountOut.get(),
amountIn: data.amountIn.get(),
amountOut: if data.amountOut.isSome: data.amountOut.get() else: zeroValue,
amountIn: if data.amountIn.isSome: data.amountIn.get() else: zeroValue,
tokenOut: data.tokenOut,
tokenIn: data.tokenIn,
symbolOut: data.symbolOut,
@ -440,7 +490,8 @@ proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.
chainIdOut: data.chainIdOut,
chainIdIn: data.chainIdIn,
transferType: data.transferType,
communityId: data.communityId
communityId: data.communityId,
isNew: data.isNew
)
proc `$`*(self: ActivityEntry): string =
@ -464,7 +515,8 @@ proc `$`*(self: ActivityEntry): string =
chainIdOut* {$self.chainIdOut},
chainIdIn* {$self.chainIdIn},
transferType* {$self.transferType},
communityId* {$self.communityId}
communityId* {$self.communityId},
isNew* {$self.isNew},
)"""
proc fromJson*(e: JsonNode, T: typedesc[FilterResponse]): FilterResponse {.inline.} =
@ -486,14 +538,77 @@ proc fromJson*(e: JsonNode, T: typedesc[FilterResponse]): FilterResponse {.inlin
errorCode: ErrorCode(e["errorCode"].getInt())
)
rpc(filterActivityAsync, "wallet"):
requestId: int32
proc fromJson*(e: JsonNode, T: typedesc[EntryUpdate]): T {.inline.} =
const posField = "pos"
const entryField = "entry"
result = T(
pos: if e.hasKey(posField): e[posField].getInt() else: -1,
entry: if e.hasKey(entryField): fromJson(e[entryField], ActivityEntry) else: ActivityEntry()
)
proc fromJson*(e: JsonNode, T: typedesc[SessionUpdate]): T {.inline.} =
const hasNewOnTopField = "hasNewOnTop"
const newField = "new"
const removedField = "removed"
let hasNewOnTop = e.hasKey(hasNewOnTopField) and e[hasNewOnTopField].getBool()
let newEntries = if e.hasKey(newField): fromJson(e[newField], seq[EntryUpdate]) else: @[]
let removed = if e.hasKey(removedField): fromJson(e[removedField], seq[TransactionIdentity]) else: @[]
result = T(
hasNewOnTop: hasNewOnTop,
`new`: newEntries,
removed: removed
)
rpc(startActivityFilterSession, "wallet"):
addresses: seq[string]
allAddresses: bool
chainIds: seq[ChainId]
filter: ActivityFilter
offset: int
limit: int
count: int
rpc(updateActivityFilterForSession, "wallet"):
sessionId: int32
filter: ActivityFilter
count: int
rpc(resetActivityFilterSession, "wallet"):
sessionId: int32
count: int
rpc(getMoreForActivityFilterSession, "wallet"):
sessionId: int32
count: int
rpc(stopActivityFilterSession, "wallet"):
sessionId: int32
# returns (sessionId, success)
proc newActivityFilterSession*(
addresses: seq[string],
allAddresses: bool,
chainIds: seq[ChainId],
filter: ActivityFilter,
count: int,
): (int32, bool) {.inline.} =
try:
let res = startActivityFilterSession(addresses, allAddresses, chainIds, filter, count)
if res.error != nil:
error "error starting a new session of activity fitlering: ", res.error
return (int32(-1), false)
return (int32(res.result.getInt()), true)
except:
return (int32(-1), false)
proc updateFilterForSession*(sessionId: int32, filter: ActivityFilter, count: int): bool {.inline.} =
try:
let res = updateActivityFilterForSession(sessionId, filter, count)
if res.error != nil:
error "error updating fitler for session: ", res.error
return false
except:
return false
return true
# see services/wallet/activity/service.go GetRecipientsResponse
type GetRecipientsResponse* = object
@ -540,7 +655,7 @@ rpc(getOldestActivityTimestampAsync, "wallet"):
requestId: int32
addresses: seq[string]
type
type
# Mirrors services/wallet/thirdparty/collectible_types.go ContractID
ContractID* = ref object of RootObj
chainID*: int

View File

@ -111,6 +111,13 @@ type
hash*: string
address*: string
proc fromJson*(t: JsonNode, T: typedesc[TransactionIdentity]): T {.inline.} =
result = TransactionIdentity(
chainId: if t.hasKey("chainId"): t["chainId"].getInt() else: 0,
hash: if t.hasKey("hash"): t["hash"].getStr() else: "",
address: if t.hasKey("address"): t["address"].getStr() else: "",
)
proc hash*(ti: TransactionIdentity): Hash =
var h: Hash = 0
h = h !& hash(ti.chainId)

View File

@ -33,7 +33,6 @@ type
multiTxType* {.serializedFieldName("type").}: MultiTransactionType
# Mirrors the transfer events from status-go, services/wallet/transfer/commands.go
const EventNewTransfers*: string = "new-transfers"
const EventFetchingRecentHistory*: string = "recent-history-fetching"
const EventRecentHistoryReady*: string = "recent-history-ready"
const EventFetchingHistoryError*: string = "fetching-history-error"
@ -41,7 +40,6 @@ const EventNonArchivalNodeDetected*: string = "non-archival-node-detected"
# Mirrors the pending transfer event from status-go, status-go/services/wallet/transfer/transaction.go
const EventPendingTransactionUpdate*: string = "pending-transaction-update"
const EventMTTransactionUpdate*: string = "multi-transaction-update"
proc `$`*(self: MultiTransactionDto): string =
return fmt"""MultiTransactionDto(

View File

@ -11,6 +11,7 @@ import shared.controls 1.0
SplitView {
id: root
// mirrors ActivityEntry defined in src/app/modules/main/wallet_section/activity/entry.nim
readonly property QtObject mockupModelData: QtObject {
readonly property int timestamp: Date.now() / 1000
readonly property int status: ctrlStatus.currentValue
@ -38,6 +39,12 @@ SplitView {
readonly property string chainId: "NETWORKID"
readonly property string chainIdIn: "NETWORKID-IN"
readonly property string chainIdOut: "NETWORKID-OUT"
readonly property bool highlight: _highlight
function doneHighlighting() {
_highlight = false
}
property bool _highlight: false
}
SplitView {
@ -175,6 +182,13 @@ SplitView {
id: ctrlMultiTrans
text: "Multi transaction"
}
Button {
text: "New transaction"
onClicked: {
mockupModelData._highlight = true
}
}
}
}
}

View File

@ -0,0 +1,54 @@
import unittest, json, options
import backend/activity
const testOneNewJsonData = "{\"new\": [{\"pos\": 3, \"entry\": {\"payloadType\": 1, \"id\": 12, \"activityType\": 1, \"activityStatus\": 2, \"timestamp\": 1234567890, \"isNew\": true}}]}"
const testOneNewJsonDataMissingIsNew = "{\"new\": [{\"pos\": 3, \"entry\": {\"payloadType\": 1, \"id\": 12, \"activityType\": 1, \"activityStatus\": 2, \"timestamp\": 1234567890}}]}"
const oneRemovedJsonTestData = "{\"removed\":[{\"chainId\": 7, \"hash\": \"0x5\", \"address\": \"0x6\"}]}"
const testAllSetJsonData = "{\"hasNewOnTop\": true, \"new\": [{\"pos\": 3, \"entry\": {\"payloadType\": 1, \"id\": 12, \"activityType\": 1, \"activityStatus\": 2, \"timestamp\": 1234567890, \"isNew\": true}}], \"removed\":[{\"chainId\": 7, \"hash\": \"0x5\", \"address\": \"0x6\"}]}"
suite "activity filter API json parsing":
test "just hasNewOnTop":
const jsonData = "{\"hasNewOnTop\": true}"
let jsonNode = json.parseJson(jsonData)
let parsed = fromJson(jsonNode, activity.SessionUpdate)
check(parsed.hasNewOnTop == true)
check(len(parsed.new) == 0)
test "just new":
let jsonNode = json.parseJson(testOneNewJsonData)
let parsed = fromJson(jsonNode, activity.SessionUpdate)
check(len(parsed.new) == 1)
let update = parsed.new[0]
check(update.pos == 3)
check(update.entry.isNew == true)
check(update.entry.getMultiTransactionId().get(-1) == 12)
check(update.entry.timestamp == 1234567890)
test "just isNew optional":
let jsonNode = json.parseJson(testOneNewJsonDataMissingIsNew)
let parsed = fromJson(jsonNode, activity.SessionUpdate)
check(len(parsed.new) == 1)
check(parsed.new[0].entry.isNew == false)
test "just removed":
let jsonNode = json.parseJson(oneRemovedJsonTestData)
let parsed = fromJson(jsonNode, activity.SessionUpdate)
check(len(parsed.removed) == 1)
let removed = parsed.removed[0]
check(removed.chainId == 7)
check(removed.hash == "0x5")
check(removed.address == "0x6")
test "all set":
let jsonNode = json.parseJson(testAllSetJsonData)
let parsed = fromJson(jsonNode, activity.SessionUpdate)
check(parsed.hasNewOnTop == true)
check(len(parsed.new) == 1)
check(len(parsed.removed) == 1)

View File

@ -114,6 +114,7 @@ func WaitForWalletEvents(eventQueue chan GoEvent, eventNames []walletevent.Event
// WaitForWalletEvents waits for the given events to be received on the eventQueue.
// It returns the wallet events in the order they are received.
// If a condition is provided, only returning true on the respective call will discard that event.
func WaitForWalletEventsWithOptionals(eventQueue chan GoEvent, eventNames []walletevent.EventType, timeout time.Duration, condition func(walletEvent *walletevent.Event) bool, optionalEventNames []walletevent.EventType) (walletEvents []*walletevent.Event, err error) {
if len(eventNames) == 0 {
return nil, errors.New("no event names provided")

View File

@ -4,7 +4,6 @@
package wallet
import (
"encoding/json"
"testing"
"time"
@ -42,13 +41,14 @@ func TestActivityIncrementalUpdates_NoFilterNewPendingTransactions(t *testing.T)
// Wait for EventActivitySessionUpdated signal triggered by the first EventPendingTransactionUpdate
update, err := helpers.WaitForWalletEventGetPayload[activity.SessionUpdate](td.eventQueue, activity.EventActivitySessionUpdated, 60*time.Second)
require.NoError(t, err)
require.NotNil(t, update.HasNewEntries)
require.True(t, *update.HasNewEntries)
require.NotNil(t, update.HasNewOnTop)
require.True(t, *update.HasNewOnTop)
// TODO #12120 check EventActivitySessionUpdated due to transactions.EventPendingTransactionStatusChanged
// statusPayload, err := helpers.WaitForWalletEventGetPayload[transactions.StatusChangedPayload](td.eventQueue, , 60*time.Second)
// TODO #12120 check EventActivitySessionUpdated due to EventPendingTransactionStatusChanged
// statusPayload, err := helpers.WaitForWalletEventGetPayload[transactions.StatusChangedPayload](td.eventQueue, activity.EventActivitySessionUpdated, 60*time.Second)
// require.NoError(t, err)
// require.Equal(t, transactions.Success, statusPayload.Status)
// require.NotNil(t, update.HasNewOnTop)
// require.True(t, *update.HasNewOnTop)
// Start history download to cleanup pending transactions
_, err = helpers.CallPrivateMethod("wallet_checkRecentHistoryForChainIDs", []interface{}{[]uint64{5}, []types.Address{td.sender.Address, td.recipient.Address}})
@ -65,16 +65,13 @@ func TestActivityIncrementalUpdates_NoFilterNewPendingTransactions(t *testing.T)
120*time.Second,
func(e *walletevent.Event) bool {
if e.Type == activity.EventActivitySessionUpdated {
var parsedPayload activity.SessionUpdate
err := json.Unmarshal(([]byte)(e.Message), &parsedPayload)
update, err = walletevent.GetPayload[activity.SessionUpdate](*e)
require.NoError(t, err)
update = &parsedPayload
// TODO #12120 enable after implementing remove and update
// require.NotNil(t, update.HasNewEntries)
// require.True(t, *update.HasNewEntries)
// require.NotNil(t, update.Removed)
// require.True(t, *update.Removed)
require.NotNil(t, update.HasNewOnTop)
require.True(t, *update.HasNewOnTop)
//require.NotNil(t, update.Removed)
//require.True(t, *update.Removed)
return false
} else if e.Type == transfer.EventFetchingHistoryError {
require.Fail(t, "History download failed")
@ -87,12 +84,12 @@ func TestActivityIncrementalUpdates_NoFilterNewPendingTransactions(t *testing.T)
[]walletevent.EventType{activity.EventActivitySessionUpdated, transfer.EventFetchingHistoryError},
)
require.NoError(t, err)
require.NotNil(t, update, "EventActivitySessionUpdated signal WASN'T triggered by the second EventPendingTransactionUpdate during history download")
require.NotNil(t, update.HasNewEntries)
require.True(t, *update.HasNewEntries)
require.NotNil(t, update, "EventActivitySessionUpdated signal was triggered by the second EventPendingTransactionUpdate during history download")
require.NotNil(t, update.HasNewOnTop)
require.True(t, *update.HasNewOnTop)
// Start history download to cleanup pending transactions
_, err = helpers.CallPrivateMethodAndGetT[interface{}]("wallet_resetFilterSession", []interface{}{sessionID, 3})
_, err = helpers.CallPrivateMethodAndGetT[interface{}]("wallet_resetActivityFilterSession", []interface{}{sessionID, 3})
require.NoError(t, err)
updatedRes, err := helpers.WaitForWalletEventsGetMap(td.eventQueue, []walletevent.EventType{activity.EventActivityFilteringDone}, 1*time.Second)

View File

@ -158,7 +158,7 @@ StatusListItem {
bgWidth: width + 2
bgHeight: height + 2
bgRadius: bgWidth / 2
bgColor: Style.current.name === Constants.lightThemeName && Constants.isDefaultTokenIcon(root.tokenImage) ?
bgColor: d.lightTheme && Constants.isDefaultTokenIcon(root.tokenImage) ?
Theme.palette.white : "transparent"
color: "transparent"
isImage: !loading
@ -174,6 +174,9 @@ StatusListItem {
property int titlePixelSize: 15
property int subtitlePixelSize: 13
property bool showRetryButton: false
readonly property bool isLightTheme: Style.current.name === Constants.lightThemeName
property color animatedBgColor
}
function getDetailsString(detailsObj) {
@ -530,6 +533,12 @@ StatusListItem {
rightPadding: 16
enabled: !loading
loading: !isModelDataValid
color: {
if (bgColorAnimation.running) {
return d.animatedBgColor
}
return sensor.containsMouse ? Theme.palette.baseColor5 : Style.current.transparent
}
statusListItemIcon.active: (loading || root.asset.name)
asset {
@ -863,4 +872,29 @@ StatusListItem {
}
}
]
ColorAnimation {
id: bgColorAnimation
target: d
property: "animatedBgColor"
from: d.isLightTheme ? "#33869eff" : "#1a4360df"
to: "transparent"
duration: 1000
alwaysRunToEnd: true
onStopped: {
modelData.doneHighlighting()
}
}
// Add a delay before the animation to make it easier to notice when scrolling
Timer {
id: delayAnimation
interval: 250
running: root.visible && isModelDataValid && modelData.highlight
repeat: false
onTriggered: {
bgColorAnimation.start()
}
}
}

View File

@ -32,8 +32,8 @@ QtObject {
property var marketValueStore: TokenMarketValuesStore{}
property var allNetworks: networksModule.all
function resetFilter() {
walletSectionInst.activityController.updateFilter()
function resetActivityData() {
walletSectionInst.activityController.resetActivityData()
}
// TODO remove all these by linking chainId for networks and activity using LeftJoinModel
@ -160,7 +160,7 @@ QtObject {
walletSectionInst.activityController.loadMoreItems()
}
function updateTransactionFilter() {
function updateTransactionFilterIfDirty() {
if (transactionActivityStatus.isFilterDirty)
walletSectionInst.activityController.updateFilter()
}

View File

@ -64,7 +64,7 @@ ColumnLayout {
target: RootStore.transactionActivityStatus
enabled: root.visible
function onIsFilterDirtyChanged() {
RootStore.updateTransactionFilter()
RootStore.updateTransactionFilterIfDirty()
}
function onFilterChainsChanged() {
WalletStores.RootStore.currentActivityFiltersStore.updateCollectiblesModel()
@ -90,13 +90,7 @@ ColumnLayout {
property bool firstSectionHeaderLoaded: false
property double lastRefreshTime
readonly property int maxSecondsBetweenRefresh: 3
function refreshData() {
RootStore.resetFilter()
d.lastRefreshTime = Date.now()
newTransactions.visible = false
}
property string openTxDetailsHash
@ -272,8 +266,8 @@ ColumnLayout {
text: qsTr("New transactions")
visible: false
onClicked: d.refreshData()
visible: RootStore.newDataAvailable && !RootStore.loadingHistoryTransactions
onClicked: RootStore.resetActivityData()
icon.name: "arrow-up"
@ -281,41 +275,6 @@ ColumnLayout {
type: StatusButton.Primary
size: StatusBaseButton.Size.Tiny
}
Connections {
target: RootStore
function onNewDataAvailableChanged() {
if (!d.lastRefreshTime || ((Date.now() - d.lastRefreshTime) > (1000 * d.maxSecondsBetweenRefresh))) {
// Show `New transactions` button only when filter is applied
if (!WalletStores.RootStore.currentActivityFiltersStore.filtersSet) {
d.refreshData()
return
}
newTransactions.visible = RootStore.newDataAvailable
return
}
if (showRefreshButtonTimer.running) {
if (!RootStore.newDataAvailable) {
showRefreshButtonTimer.stop()
newTransactions.visible = false
}
} else if(RootStore.newDataAvailable) {
showRefreshButtonTimer.start()
}
}
}
Timer {
id: showRefreshButtonTimer
interval: 2000
running: false
repeat: false
onTriggered: newTransactions.visible = RootStore.newDataAvailable
}
}
StatusMenu {

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 6522d52016eae3d848c228a82d79dde902d69688
Subproject commit e1c7c715aa3701791c5280e5c7869af7675bc1ee