From 9262943176583870bb135b8bb91dfe71fcb0883d Mon Sep 17 00:00:00 2001 From: Stefan Date: Thu, 11 May 2023 10:56:55 +0300 Subject: [PATCH] feat(wallet) complete the filter API Bumps status-go HEAD to include required changes Updates Nim filter components and APIs to follow API changes in status-go Complete the debugging code Add TODO placeholders to be completed in follow up PRs: collectibles ... General improvements and refactoring Closes #10634 --- nim.cfg | 2 +- .../wallet_section/activity/controller.nim | 149 ++++++-- .../main/wallet_section/activity/entry.nim | 26 +- .../transactions/multi_transaction_item.nim | 12 +- .../wallet_section/transactions/utils.nim | 2 +- src/app_service/service/transaction/dto.nim | 2 +- .../service/transaction/service.nim | 4 +- src/backend/activity.nim | 128 +++++-- src/backend/backend.nim | 17 +- src/backend/transactions.nim | 2 +- .../AppLayouts/Wallet/views/RightTabView.qml | 5 +- ui/imports/shared/views/ActivityView.qml | 337 ++++++++++++++++-- 12 files changed, 562 insertions(+), 124 deletions(-) diff --git a/nim.cfg b/nim.cfg index 8b0f41ec61..bb411c914d 100644 --- a/nim.cfg +++ b/nim.cfg @@ -1,4 +1,4 @@ # we need to link C++ libraries gcc.linkerexe="g++" -path = "src" \ No newline at end of file +path = "src" diff --git a/src/app/modules/main/wallet_section/activity/controller.nim b/src/app/modules/main/wallet_section/activity/controller.nim index d66fbe71fb..4a81e160e8 100644 --- a/src/app/modules/main/wallet_section/activity/controller.nim +++ b/src/app/modules/main/wallet_section/activity/controller.nim @@ -1,4 +1,5 @@ import NimQml, logging, std/json, sequtils, sugar, options +import tables import model import entry @@ -22,6 +23,9 @@ QtObject: model: Model transactionsModule: transactions_module.AccessInterface currentActivityFilter: backend_activity.ActivityFilter + # TODO remove chains and addresses to use the app one + addresses: seq[string] + chainIds: seq[int] proc setup(self: Controller) = self.QObject.setup @@ -43,15 +47,15 @@ QtObject: read = getModel # TODO: move it to service, make it async and lazy load details for transactions - proc backendToPresentation(self: Controller, backendEnties: seq[backend_activity.ActivityEntry]): seq[entry.ActivityEntry] = + proc backendToPresentation(self: Controller, backendEntities: seq[backend_activity.ActivityEntry]): seq[entry.ActivityEntry] = var multiTransactionsIds: seq[int] = @[] var transactionIdentities: seq[backend.TransactionIdentity] = @[] var pendingTransactionIdentities: seq[backend.TransactionIdentity] = @[] # Extract metadata required to fetch details # TODO: temporary here to show the working API. Will be done as required on a detail request from UI - for backendEntry in backendEnties: - case backendEntry.transactionType: + for backendEntry in backendEntities: + case backendEntry.payloadType: of MultiTransaction: multiTransactionsIds.add(backendEntry.id) of SimpleTransaction: @@ -59,11 +63,13 @@ QtObject: of PendingTransaction: pendingTransactionIdentities.add(backendEntry.transaction.get()) - var multiTransactions: seq[MultiTransactionDto] = @[] + var multiTransactions = initTable[int, MultiTransactionDto]() if len(multiTransactionsIds) > 0: - multiTransactions = transaction_service.getMultiTransactions(multiTransactionsIds) + let mts = transaction_service.getMultiTransactions(multiTransactionsIds) + for mt in mts: + multiTransactions[mt.id] = mt - var transactions: seq[Item] = @[] + var transactions = initTable[TransactionIdentity, ref Item]() if len(transactionIdentities) > 0: let response = backend.getTransfersForIdentities(transactionIdentities) let res = response.result @@ -71,9 +77,11 @@ QtObject: raise newException(Defect, "failed fetching transaction details") let transactionsDtos = res.getElems().map(x => x.toTransactionDto()) - transactions = self.transactionsModule.transactionsToItems(transactionsDtos, @[]) + let trItems = self.transactionsModule.transactionsToItems(transactionsDtos, @[]) + for item in trItems: + transactions[TransactionIdentity(chainId: item.getChainId(), hash: item.getId(), address: item.getAddress())] = toRef(item) - var pendingTransactions: seq[Item] = @[] + var pendingTransactions = initTable[TransactionIdentity, ref Item]() if len(pendingTransactionIdentities) > 0: let response = backend.getPendingTransactionsForIdentities(pendingTransactionIdentities) let res = response.result @@ -81,33 +89,43 @@ QtObject: raise newException(Defect, "failed fetching pending transactions details") let pendingTransactionsDtos = res.getElems().map(x => x.toPendingTransactionDto()) - pendingTransactions = self.transactionsModule.transactionsToItems(pendingTransactionsDtos, @[]) + let trItems = self.transactionsModule.transactionsToItems(pendingTransactionsDtos, @[]) + for item in trItems: + pendingTransactions[TransactionIdentity(chainId: item.getChainId(), hash: item.getId(), address: item.getAddress())] = toRef(item) # Merge detailed transaction info in order - result = newSeq[entry.ActivityEntry](multiTransactions.len + transactions.len + pendingTransactions.len) + result = newSeqOfCap[entry.ActivityEntry](multiTransactions.len + transactions.len + pendingTransactions.len) var mtIndex = 0 var tIndex = 0 var ptIndex = 0 - for i in low(backendEnties) .. high(backendEnties): - let backendEntry = backendEnties[i] - case backendEntry.transactionType: + for backendEntry in backendEntities: + case backendEntry.payloadType: of MultiTransaction: - result[i] = entry.newMultiTransactionActivityEntry(multiTransactions[mtIndex]) + let id = multiTransactionsIds[mtIndex] + if multiTransactions.hasKey(id): + result.add(entry.newMultiTransactionActivityEntry(multiTransactions[id], backendEntry)) + else: + error "failed to find multi transaction with id: ", id mtIndex += 1 of SimpleTransaction: - let refInstance = new(Item) - refInstance[] = transactions[tIndex] - result[i] = entry.newTransactionActivityEntry(refInstance, false) + let identity = transactionIdentities[tIndex] + if transactions.hasKey(identity): + result.add(entry.newTransactionActivityEntry(transactions[identity], backendEntry)) + else: + error "failed to find transaction with identity: ", identity tIndex += 1 of PendingTransaction: - let refInstance = new(Item) - refInstance[] = pendingTransactions[ptIndex] - result[i] = entry.newTransactionActivityEntry(refInstance, true) + let identity = pendingTransactionIdentities[ptIndex] + if pendingTransactions.hasKey(identity): + result.add(entry.newTransactionActivityEntry(pendingTransactions[identity], backendEntry)) + else: + error "failed to find pending transaction with identity: ", identity ptIndex += 1 - proc refreshData*(self: Controller) {.slot.} = + proc refreshData(self: Controller) = + # result type is RpcResponse - let response = backend_activity.getActivityEntries(@["0x0000000000000000000000000000000000000001"], @[1], self.currentActivityFilter, 0, 10) + let response = backend_activity.getActivityEntries(self.addresses, self.chainIds, self.currentActivityFilter, 0, 10) # RPC returns null for result in case of empty array if response.error != nil or (response.result.kind != JArray and response.result.kind != JNull): error "error fetching activity entries: ", response.error @@ -117,15 +135,88 @@ QtObject: self.model.setEntries(@[]) return - var backendEnties = newSeq[backend_activity.ActivityEntry](response.result.len) + var backendEntities = newSeq[backend_activity.ActivityEntry](response.result.len) for i in 0 ..< response.result.len: - backendEnties[i] = fromJson(response.result[i], backend_activity.ActivityEntry) - let entries = self.backendToPresentation(backendEnties) + backendEntities[i] = fromJson(response.result[i], backend_activity.ActivityEntry) + let entries = self.backendToPresentation(backendEntities) self.model.setEntries(entries) - # TODO: add all parameters and separate in different methods - proc updateFilter*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} = - # Update filter + proc updateFilter*(self: Controller) {.slot.} = + self.refreshData() + + proc setFilterTime*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} = self.currentActivityFilter.period = backend_activity.newPeriod(startTimestamp, endTimestamp) - self.refreshData() + proc setFilterType*(self: Controller, typesArrayJsonString: string) {.slot.} = + let typesJson = parseJson(typesArrayJsonString) + if typesJson.kind != JArray: + error "invalid array of json ints" + return + + var types = newSeq[backend_activity.ActivityType](typesJson.len) + for i in 0 ..< typesJson.len: + types[i] = backend_activity.ActivityType(typesJson[i].getInt()) + + self.currentActivityFilter.types = types + + proc setFilterStatus*(self: Controller, statusesArrayJsonString: string) {.slot.} = + let statusesJson = parseJson(statusesArrayJsonString) + if statusesJson.kind != JArray: + error "invalid array of json ints" + return + + var statuses = newSeq[backend_activity.ActivityStatus](statusesJson.len) + for i in 0 ..< statusesJson.len: + statuses[i] = backend_activity.ActivityStatus(statusesJson[i].getInt()) + + self.currentActivityFilter.statuses = statuses + + proc setFilterToAddresses*(self: Controller, addressesArrayJsonString: string) {.slot.} = + let addressesJson = parseJson(addressesArrayJsonString) + if addressesJson.kind != JArray: + error "invalid array of json strings" + return + + var addresses = newSeq[string](addressesJson.len) + for i in 0 ..< addressesJson.len: + addresses[i] = addressesJson[i].getStr() + + self.currentActivityFilter.counterpartyAddresses = addresses + + proc setFilterAssets*(self: Controller, assetsArrayJsonString: string) {.slot.} = + let assetsJson = parseJson(assetsArrayJsonString) + if assetsJson.kind != JArray: + error "invalid array of json strings" + return + + var assets = newSeq[TokenCode](assetsJson.len) + for i in 0 ..< assetsJson.len: + assets[i] = TokenCode(assetsJson[i].getStr()) + + self.currentActivityFilter.tokens.assets = option(assets) + + # TODO: remove me and use ground truth + proc setFilterAddresses*(self: Controller, addressesArrayJsonString: string) {.slot.} = + let addressesJson = parseJson(addressesArrayJsonString) + if addressesJson.kind != JArray: + error "invalid array of json strings" + return + + var addresses = newSeq[string](addressesJson.len) + for i in 0 ..< addressesJson.len: + addresses[i] = addressesJson[i].getStr() + + self.addresses = addresses + + # TODO: remove me and use ground truth + proc setFilterChains*(self: Controller, chainIdsArrayJsonString: string) {.slot.} = + let chainIdsJson = parseJson(chainIdsArrayJsonString) + if chainIdsJson.kind != JArray: + error "invalid array of json ints" + return + + var chainIds = newSeq[int](chainIdsJson.len) + for i in 0 ..< chainIdsJson.len: + chainIds[i] = chainIdsJson[i].getInt() + + self.chainIds = chainIds diff --git a/src/app/modules/main/wallet_section/activity/entry.nim b/src/app/modules/main/wallet_section/activity/entry.nim index 4609e26808..0230f83245 100644 --- a/src/app/modules/main/wallet_section/activity/entry.nim +++ b/src/app/modules/main/wallet_section/activity/entry.nim @@ -3,34 +3,44 @@ import NimQml, tables, json, strformat, sequtils, strutils, logging import ../transactions/view import ../transactions/item import ./backend/transactions +import backend/activity as backend -# The ActivityEntry contains one of the following instances transaction, pensing transaction or multi-transaction # It is used to display an activity history entry in the QML UI +# +# TODO add all required metadata from filtering +# +# 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 +# +# Outdated: The ActivityEntry contains one of the following instances transaction, pending transaction or multi-transaction QtObject: type ActivityEntry* = ref object of QObject + # TODO: these should be removed multi_transaction: MultiTransactionDto transaction: ref Item isPending: bool + metadata: backend.ActivityEntry + proc setup(self: ActivityEntry) = self.QObject.setup proc delete*(self: ActivityEntry) = self.QObject.delete - proc newMultiTransactionActivityEntry*(mt: MultiTransactionDto): ActivityEntry = + proc newMultiTransactionActivityEntry*(mt: MultiTransactionDto, metadata: backend.ActivityEntry): ActivityEntry = new(result, delete) result.multi_transaction = mt result.transaction = nil result.isPending = false result.setup() - proc newTransactionActivityEntry*(tr: ref Item, isPending: bool): ActivityEntry = + proc newTransactionActivityEntry*(tr: ref Item, metadata: backend.ActivityEntry): ActivityEntry = new(result, delete) result.multi_transaction = nil result.transaction = tr - result.isPending = isPending + result.isPending = metadata.payloadType == backend.PayloadType.PendingTransaction result.setup() proc isMultiTransaction*(self: ActivityEntry): bool {.slot.} = @@ -124,4 +134,10 @@ QtObject: QtProperty[int] timestamp: read = getTimestamp - # TODO: properties - type, fromChains, toChains, fromAsset, toAsset, assetName \ No newline at end of file + # TODO: properties - type, fromChains, toChains, fromAsset, toAsset, assetName + + # proc getType*(self: ActivityEntry): int {.slot.} = + # return self.metadata.activityType.int + + # QtProperty[int] type: + # read = getType \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/transactions/multi_transaction_item.nim b/src/app/modules/main/wallet_section/transactions/multi_transaction_item.nim index b77b4b6020..7baeee3cbe 100644 --- a/src/app/modules/main/wallet_section/transactions/multi_transaction_item.nim +++ b/src/app/modules/main/wallet_section/transactions/multi_transaction_item.nim @@ -14,7 +14,7 @@ type fromAsset: string toAsset: string fromAmount: string - multiTxtype: MultiTransactionType + multiTxType: MultiTransactionType proc initMultiTransactionItem*( id: int, @@ -24,7 +24,7 @@ proc initMultiTransactionItem*( fromAsset: string, toAsset: string, fromAmount: string, - multiTxtype: MultiTransactionType, + multiTxType: MultiTransactionType, ): MultiTransactionItem = result.id = id result.timestamp = timestamp @@ -33,7 +33,7 @@ proc initMultiTransactionItem*( result.fromAsset = fromAsset result.toAsset = toAsset result.fromAmount = fromAmount - result.multiTxtype = multiTxtype + result.multiTxType = multiTxType proc `$`*(self: MultiTransactionItem): string = result = fmt"""MultiTransactionItem( @@ -44,7 +44,7 @@ proc `$`*(self: MultiTransactionItem): string = fromAsset: {self.fromAsset}, toAsset: {self.toAsset}, fromAmount: {self.fromAmount}, - multiTxtype: {self.multiTxtype}, + multiTxType: {self.multiTxType}, ]""" proc getId*(self: MultiTransactionItem): int = @@ -68,5 +68,5 @@ proc getToAsset*(self: MultiTransactionItem): string = proc getFromAmount*(self: MultiTransactionItem): string = return self.fromAmount -proc getMultiTxtype*(self: MultiTransactionItem): MultiTransactionType = - return self.multiTxtype \ No newline at end of file +proc getMultiTxType*(self: MultiTransactionItem): MultiTransactionType = + return self.multiTxType \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/transactions/utils.nim b/src/app/modules/main/wallet_section/transactions/utils.nim index 78675af7ce..2fa585269e 100644 --- a/src/app/modules/main/wallet_section/transactions/utils.nim +++ b/src/app/modules/main/wallet_section/transactions/utils.nim @@ -93,5 +93,5 @@ proc multiTransactionToItem*(t: MultiTransactionDto): MultiTransactionItem = t.fromAsset, t.toAsset, t.fromAmount, - t.multiTxtype + t.multiTxType ) \ No newline at end of file diff --git a/src/app_service/service/transaction/dto.nim b/src/app_service/service/transaction/dto.nim index e05ec9770f..725bc805de 100644 --- a/src/app_service/service/transaction/dto.nim +++ b/src/app_service/service/transaction/dto.nim @@ -151,7 +151,7 @@ proc toMultiTransactionDto*(jsonObj: JsonNode): MultiTransactionDto = discard jsonObj.getProp("fromAmount", result.fromAmount) var multiTxType: int discard jsonObj.getProp("type", multiTxType) - result.multiTxtype = cast[MultiTransactionType](multiTxType) + result.multiTxType = cast[MultiTransactionType](multiTxType) proc cmpTransactions*(x, y: TransactionDto): int = # Sort proc to compare transactions from a single account. diff --git a/src/app_service/service/transaction/service.nim b/src/app_service/service/transaction/service.nim index 151022b977..0083ce2c5a 100644 --- a/src/app_service/service/transaction/service.nim +++ b/src/app_service/service/transaction/service.nim @@ -365,7 +365,7 @@ QtObject: fromAsset: tokenSymbol, toAsset: tokenSymbol, fromAmount: "0x" & amountToSend.toHex, - multiTxtype: transactions.MultiTransactionType.MultiTransactionSend, + multiTxType: transactions.MultiTransactionType.MultiTransactionSend, ), paths, password, @@ -432,7 +432,7 @@ QtObject: fromAsset: tokenSymbol, toAsset: tokenSymbol, fromAmount: "0x" & amountToSend.toHex, - multiTxtype: transactions.MultiTransactionType.MultiTransactionSend, + multiTxType: transactions.MultiTransactionType.MultiTransactionSend, ), paths, password, diff --git a/src/backend/activity.nim b/src/backend/activity.nim index 6dde3f4bdc..d089801170 100644 --- a/src/backend/activity.nim +++ b/src/backend/activity.nim @@ -1,18 +1,20 @@ -import times, strformat +import times, strformat, options import json, json_serialization -import options -import ./core, ./response_type -from ./gen import rpc -import ./backend +import core, response_type +from gen import rpc +import backend import transactions export response_type +# see status-go/services/wallet/activity/filter.go NoLimitTimestampForPeriod +const noLimitTimestampForPeriod = 0 + # TODO: consider using common status-go types via protobuf # TODO: consider using flags instead of list of enums type Period* = object - startTimestamp*: int + startTimestamp* : int endTimestamp*: int # see status-go/services/wallet/activity/filter.go Type @@ -27,37 +29,89 @@ type TokenType* {.pure.} = enum Asset, Collectibles + # see status-go/services/wallet/activity/filter.go TokenCode, TokenAddress + TokenCode* = distinct string + # Not used for now until collectibles are supported in the backend. TODO: extend this with chain ID and token ID + TokenAddress* = distinct string + + # see status-go/services/wallet/activity/filter.go Tokens + # All empty sequences or none Options mean include all + Tokens* = object + assets*: Option[seq[TokenCode]] + collectibles*: Option[seq[TokenAddress]] + enabledTypes*: seq[TokenType] + # see status-go/services/wallet/activity/filter.go Filter + # All empty sequences mean include all ActivityFilter* = object - period* {.serializedFieldName("period").}: Period - types* {.serializedFieldName("types").}: seq[ActivityType] - statuses* {.serializedFieldName("statuses").}: seq[ActivityStatus] - tokenTypes* {.serializedFieldName("tokenTypes").}: seq[TokenType] - counterpartyAddresses* {.serializedFieldName("counterpartyAddresses").}: seq[string] + period*: Period + types*: seq[ActivityType] + statuses*: seq[ActivityStatus] + tokens*: Tokens + counterpartyAddresses*: seq[string] + +proc toJson[T](obj: Option[T]): JsonNode = + if obj.isSome: + toJson(obj.get()) + else: + newJNull() + +proc fromJson[T](jsonObj: JsonNode, TT: typedesc[Option[T]]): Option[T] = + if jsonObj.kind != JNull: + return some(to(jsonObj, T)) + else: + return none(T) + +proc `%`*(at: ActivityType): JsonNode {.inline.} = + return newJInt(ord(at)) + +proc `%`*(aSt: ActivityStatus): JsonNode {.inline.} = + return newJInt(ord(aSt)) + +proc `$`*(tc: TokenCode): string = $(string(tc)) +proc `$`*(ta: TokenAddress): string = $(string(ta)) + +proc `%`*(tc: TokenCode): JsonNode {.inline.} = + return %(string(tc)) + +proc `%`*(ta: TokenAddress): JsonNode {.inline.} = + return %(string(ta)) + +proc parseJson*(tc: var TokenCode, node: JsonNode) = + tc = TokenCode(node.getStr) + +proc parseJson*(ta: var TokenAddress, node: JsonNode) = + ta = TokenAddress(node.getStr) + +proc newAllTokens(): Tokens = + result.assets = none(seq[TokenCode]) + result.collectibles = none(seq[TokenAddress]) proc newPeriod*(startTime: Option[DateTime], endTime: Option[DateTime]): Period = if startTime.isSome: result.startTimestamp = startTime.get().toTime().toUnix().int else: - result.startTimestamp = 0 + result.startTimestamp = noLimitTimestampForPeriod if endTime.isSome: result.endTimestamp = endTime.get().toTime().toUnix().int else: - result.endTimestamp = 0 + result.endTimestamp = noLimitTimestampForPeriod proc newPeriod*(startTimestamp: int, endTimestamp: int): Period = result.startTimestamp = startTimestamp result.endTimestamp = endTimestamp proc getIncludeAllActivityFilter*(): ActivityFilter = - result = ActivityFilter(period: newPeriod(none(DateTime), none(DateTime)), types: @[], statuses: @[], tokenTypes: @[], counterpartyAddresses: @[]) + result = ActivityFilter(period: newPeriod(none(DateTime), none(DateTime)), types: @[], statuses: @[], + tokens: newAllTokens(), counterpartyAddresses: @[]) # Empty sequence for paramters means include all -proc newActivityFilter*(period: Period, activityType: seq[ActivityType], activityStatus: seq[ActivityStatus], tokenType: seq[TokenType], counterpartyAddress: seq[string]): ActivityFilter = +proc newActivityFilter*(period: Period, activityType: seq[ActivityType], activityStatus: seq[ActivityStatus], + tokens: Tokens, counterpartyAddress: seq[string]): ActivityFilter = result.period = period result.types = activityType result.statuses = activityStatus - result.tokenTypes = tokenType + result.tokens = tokens result.counterpartyAddresses = counterpartyAddress # Mirrors status-go/services/wallet/activity/activity.go PayloadType @@ -68,8 +122,8 @@ type PendingTransaction # Define toJson proc for PayloadType -proc toJson*(x: PayloadType): JsonNode {.inline.} = - return %*(ord(x)) +proc `%`*(x: PayloadType): JsonNode {.inline.} = + return newJInt(ord(x)) # Define fromJson proc for PayloadType proc fromJson*(x: JsonNode, T: typedesc[PayloadType]): PayloadType {.inline.} = @@ -78,23 +132,16 @@ proc fromJson*(x: JsonNode, T: typedesc[PayloadType]): PayloadType {.inline.} = # TODO: hide internals behind safe interface type ActivityEntry* = object - transactionType* {.serializedFieldName("transactionType").}: PayloadType - transaction* {.serializedFieldName("transaction").}: Option[TransactionIdentity] - id* {.serializedFieldName("id").}: int - timestamp* {.serializedFieldName("timestamp").}: int - activityType* {.serializedFieldName("activityType").}: MultiTransactionType + # Identification + payloadType*: PayloadType + transaction*: Option[TransactionIdentity] + id*: int -proc fromJson[T](jsonObj: JsonNode, TT: typedesc[Option[T]]): Option[T] = - if jsonObj.kind != JNull: - return some(to(jsonObj, T)) - else: - return none(T) - -proc toJson[T](obj: Option[T]): JsonNode = - if obj.isSome: - toJson(obj.get()) - else: - newJNull() + timestamp*: int + # TODO: change it into ActivityType + activityType*: MultiTransactionType + activityStatus*: ActivityStatus + tokenType*: TokenType # Define toJson proc for PayloadType proc toJson*(ae: ActivityEntry): JsonNode {.inline.} = @@ -103,19 +150,24 @@ proc toJson*(ae: ActivityEntry): JsonNode {.inline.} = # Define fromJson proc for PayloadType proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.} = result = T( - transactionType: fromJson(e["transactionType"], PayloadType), - transaction: if e.hasKey("transaction"): fromJson(e["transaction"], Option[TransactionIdentity]) else: none(TransactionIdentity), + payloadType: fromJson(e["payloadType"], PayloadType), + transaction: if e.hasKey("transaction"): fromJson(e["transaction"], Option[TransactionIdentity]) + else: none(TransactionIdentity), id: e["id"].getInt(), timestamp: e["timestamp"].getInt() ) proc `$`*(self: ActivityEntry): string = - let transactionStr = if self.transaction.isSome: $self.transaction.get() else: "none(TransactionIdentity)" + let transactionStr = if self.transaction.isSome: $self.transaction.get() + else: "none(TransactionIdentity)" return fmt"""ActivityEntry( - transactionType:{self.transactionType.int}, + payloadType:{$self.payloadType}, transaction:{transactionStr}, id:{self.id}, timestamp:{self.timestamp}, + activityType* {$self.activityType}, + activityStatus* {$self.activityStatus}, + tokenType* {$self.tokenType}, )""" rpc(getActivityEntries, "wallet"): diff --git a/src/backend/backend.nim b/src/backend/backend.nim index 2b2dd01b55..79795fe7b9 100644 --- a/src/backend/backend.nim +++ b/src/backend/backend.nim @@ -1,4 +1,5 @@ import json, json_serialization, strformat +import hashes import ./core, ./response_type from ./gen import rpc @@ -98,9 +99,19 @@ rpc(getPendingTransactionsByChainIDs, "wallet"): type TransactionIdentity* = ref object - chainId* {.serializedFieldName("chainId").}: int - hash* {.serializedFieldName("hash").}: string - address* {.serializedFieldName("address").}: string + chainId*: int + hash*: string + address*: string + +proc hash*(ti: TransactionIdentity): Hash = + var h: Hash = 0 + h = h !& hash(ti.chainId) + h = h !& hash(ti.hash) + h = h !& hash(ti.address) + result = !$h + +proc `==`*(a, b: TransactionIdentity): bool = + result = (a.chainId == b.chainId) and (a.hash == b.hash) and (a.address == b.address) proc `$`*(self: TransactionIdentity): string = return fmt"""TransactionIdentity( diff --git a/src/backend/transactions.nim b/src/backend/transactions.nim index 1233e5a919..d8968fd882 100644 --- a/src/backend/transactions.nim +++ b/src/backend/transactions.nim @@ -17,7 +17,7 @@ type fromAsset* {.serializedFieldName("fromAsset").}: string toAsset* {.serializedFieldName("toAsset").}: string fromAmount* {.serializedFieldName("fromAmount").}: string - multiTxtype* {.serializedFieldName("type").}: MultiTransactionType + multiTxType* {.serializedFieldName("type").}: MultiTransactionType proc getTransactionByHash*(chainId: int, hash: string): RpcResponse[JsonNode] {.raises: [Exception].} = core.callPrivateRPCWithChainId("eth_getTransactionByHash", chainId, %* [hash]) diff --git a/ui/app/AppLayouts/Wallet/views/RightTabView.qml b/ui/app/AppLayouts/Wallet/views/RightTabView.qml index 0238ca3b2a..dfc3732fcf 100644 --- a/ui/app/AppLayouts/Wallet/views/RightTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/RightTabView.qml @@ -84,6 +84,7 @@ Item { text: qsTr("Activity") } // TODO - DEV: remove me + // Enable for debugging activity filter // currentIndex: 3 // StatusTabButton { // rightPadding: 0 @@ -128,6 +129,9 @@ Item { // Layout.fillHeight: true // controller: RootStore.activityController + // networksModel: RootStore.allNetworks + // assetsModel: RootStore.assets + // assetsLoading: RootStore.assetsLoading // } } } @@ -144,7 +148,6 @@ Item { assetsLoading: RootStore.assetsLoading address: RootStore.overview.mixedcaseAddress - networkConnectionStore: root.networkConnectionStore } diff --git a/ui/imports/shared/views/ActivityView.qml b/ui/imports/shared/views/ActivityView.qml index 2a5f128043..8a6ae11b41 100644 --- a/ui/imports/shared/views/ActivityView.qml +++ b/ui/imports/shared/views/ActivityView.qml @@ -8,6 +8,8 @@ import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import StatusQ.Core.Theme 0.1 +import AppLayouts.stores 1.0 + import SortFilterProxyModel 0.2 import utils 1.0 @@ -18,10 +20,134 @@ import "../stores" import "../controls" // Temporary developer view to test the filter APIs -Item { +Control { id: root - property var controller + property var controller: null + property var networksModel: null + property var assetsModel: null + property bool assetsLoading: true + + // Mirrors src/backend/activity.nim ActivityType + enum ActivityType { + Send, + Receive, + Buy, + Swap, + Bridge + } + + // Mirrors src/backend/activity.nim ActivityStatus + enum ActivityStatus { + Failed, + Pending, + Complete, + Finalized + } + + background: Rectangle { + anchors.fill: parent + color: "white" + } + + QtObject { + id: d + + readonly property int millisInADay: 24 * 60 * 60 * 1000 + property int start: fromSlider.value > 0 + ? Math.floor(new Date(new Date() - (fromSlider.value * millisInADay)).getTime() / 1000) + : 0 + property int end: toSlider.value > 0 + ? Math.floor(new Date(new Date() - (toSlider.value * millisInADay)).getTime() / 1000) + : 0 + + function updateFilter() { + // Time + controller.setFilterTime(d.start, d.end) + + // Activity types + var types = [] + for(var i = 0; i < typeModel.count; i++) { + let item = typeModel.get(i) + if(item.checked) { + types.push(i) + } + } + controller.setFilterType(JSON.stringify(types)) + + // Activity status + var statuses = [] + for(var i = 0; i < statusModel.count; i++) { + let item = statusModel.get(i) + if(item.checked) { + statuses.push(i) + } + } + controller.setFilterStatus(JSON.stringify(statuses)) + + // Counterparty addresses + var addresses = toAddressesInput.text.split(',') + if(addresses.length == 1 && addresses[0].trim() == "") { + addresses = [] + } else { + for (var i = 0; i < addresses.length; i++) { + addresses[i] = padHexAddress(addresses[i].trim()); + } + } + controller.setFilterToAddresses(JSON.stringify(addresses)) + + // Involved addresses + var addresses = addressesInput.text.split(',') + if(addresses.length == 1 && addresses[0].trim() == "") { + addresses = [] + } else { + for (var i = 0; i < addresses.length; i++) { + addresses[i] = padHexAddress(addresses[i].trim()); + } + } + controller.setFilterAddresses(JSON.stringify(addresses)) + + // Chains + var chains = [] + for(var i = 0; i < clonedNetworksModel.count; i++) { + let item = clonedNetworksModel.get(i) + if(item.checked) { + chains.push(parseInt(item.chainId)) + } + } + controller.setFilterChains(JSON.stringify(chains)) + + // Assets + var assets = [] + if(assetsLoader.status == Loader.Ready) { + for(var i = 0; i < assetsLoader.item.count; i++) { + let item = assetsLoader.item.get(i) + if(item.checked) { + assets.push(item.symbol) + } + } + } + controller.setFilterAssets(JSON.stringify(assets)) + + // Update the model + controller.updateFilter() + } + + function padHexAddress(input) { + var addressLength = 40; + var strippedInput = input.startsWith("0x") ? input.slice(2) : input; + + if (strippedInput.length > addressLength) { + console.error("Input is longer than expected address"); + return null; + } + + var paddingLength = addressLength - strippedInput.length; + var padding = Array(paddingLength + 1).join("0"); + + return "0x" + padding + strippedInput; + } + } ColumnLayout { anchors.fill: parent @@ -29,48 +155,187 @@ Item { ColumnLayout { id: filterLayout - readonly property int millisInADay: 24 * 60 * 60 * 1000 - property int start: fromSlider.value > 0 ? Math.floor(new Date(new Date() - (fromSlider.value * millisInADay)).getTime() / 1000) : 0 - property int end: toSlider.value > 0 ? Math.floor(new Date(new Date() - (toSlider.value * millisInADay)).getTime() / 1000) : 0 + ColumnLayout { + id: timeFilterLayout - function updateFilter() { controller.updateFilter(start, end) } + RowLayout { + Label { text: "Past Days Span: 100" } + Slider { + id: fromSlider - RowLayout { - Label { text: "Past Days Span: 100" } - Slider { - id: fromSlider + Layout.preferredWidth: 200 + Layout.preferredHeight: 50 - Layout.preferredWidth: 200 - Layout.preferredHeight: 50 + from: 100 + to: 0 - from: 100 - to: 0 + stepSize: 1 + value: 0 + } + Label { text: `${fromSlider.value}d - ${toSlider.value}d` } + Slider { + id: toSlider - stepSize: 1 - value: 0 + Layout.preferredWidth: 200 + Layout.preferredHeight: 50 - onPressedChanged: { if (!pressed) filterLayout.updateFilter() } + enabled: fromSlider.value > 1 + + from: fromSlider.value - 1 + to: 0 + + stepSize: 1 + value: 0 + } + Label { text: "0" } + } + Label { text: `Interval: ${d.start > 0 ? root.epochToDateStr(d.start) : "all time"} - ${d.end > 0 ? root.epochToDateStr(d.end) : "now"}` } + } + RowLayout { + Label { text: "Type" } + // Models the ActivityType + ListModel { + id: typeModel + + ListElement { text: qsTr("Send"); checked: false } + ListElement { text: qsTr("Receive"); checked: false } + ListElement { text: qsTr("Buy"); checked: false } + ListElement { text: qsTr("Swap"); checked: false } + ListElement { text: qsTr("Bridge"); checked: false } + } + + ComboBox { + model: typeModel + + displayText: qsTr("Select types") + + currentIndex: -1 + textRole: "text" + + delegate: ItemOnOffDelegate {} + } + + Label { text: "Status" } + // ActivityStatus + ListModel { + id: statusModel + ListElement { text: qsTr("Failed"); checked: false } + ListElement { text: qsTr("Pending"); checked: false } + ListElement { text: qsTr("Complete"); checked: false } + ListElement { text: qsTr("Finalized"); checked: false } + } + + ComboBox { + displayText: qsTr("Select statuses") + + model: statusModel + + currentIndex: -1 + textRole: "text" + + delegate: ItemOnOffDelegate {} + } + + Label { text: "To addresses" } + TextField { + id: toAddressesInput + + Layout.fillWidth: true + + placeholderText: qsTr("0x1234, 0x5678, ...") + } + + Button { + text: qsTr("Update") + onClicked: d.updateFilter() + } + } + RowLayout { + + Label { text: "Addresses" } + TextField { + id: addressesInput + + Layout.fillWidth: true + + placeholderText: qsTr("0x1234, 0x5678, ...") + } + + Label { text: "Chains" } + ComboBox { + displayText: qsTr("Select chains") + + Layout.preferredWidth: 300 + + model: clonedNetworksModel + currentIndex: -1 + + delegate: ItemOnOffDelegate {} + } + + Label { text: "Assets" } + ComboBox { + displayText: assetsLoader.status != Loader.Ready ? qsTr("Loading...") : qsTr("Select an asset") + + enabled: assetsLoader.status == Loader.Ready + + Layout.preferredWidth: 300 + + model: assetsLoader.item + + currentIndex: -1 + + delegate: ItemOnOffDelegate {} + } + } + + CloneModel { + id: clonedNetworksModel + + sourceModel: root.networksModel + roles: ["layer", "chainId", "chainName"] + rolesOverride: [{ role: "text", transform: (md) => `${md.chainName} [${md.chainId}] ${md.layer}` }, + { role: "checked", transform: (md) => false }] + } + + // Found out the hard way that the assets are not loaded immediately after root.assetLoading is enabled so there is no data set yet + Timer { + id: delayAssetLoading + + property bool loadingEnabled: false + + interval: 1000; repeat: false + running: !root.assetsLoading + onTriggered: loadingEnabled = true + } + + Loader { + id: assetsLoader + + sourceComponent: CloneModel { + sourceModel: root.assetsModel + roles: ["name", "symbol", "address"] + rolesOverride: [{ role: "text", transform: (md) => `[${md.symbol}] ${md.name}`}, + { role: "checked", transform: (md) => false }] + } + active: delayAssetLoading.loadingEnabled + } + + component ItemOnOffDelegate: Item { + width: parent ? parent.width : 0 + height: itemLayout.implicitHeight + + readonly property var entry: model + + RowLayout { + id: itemLayout + anchors.fill: parent + + CheckBox { checked: entry.checked; onCheckedChanged: entry.checked = checked } + Label { text: entry.text } + RowLayout {} } - Label { text: `${fromSlider.value}d - ${toSlider.value}d` } - Slider { - id: toSlider - - Layout.preferredWidth: 200 - Layout.preferredHeight: 50 - - enabled: fromSlider.value > 1 - - from: fromSlider.value - 1 - to: 0 - - stepSize: 1 - value: 0 - - onPressedChanged: { if (!pressed) filterLayout.updateFilter() } - } - Label { text: "0" } } - Label { text: `Interval: ${filterLayout.start > 0 ? root.epochToDateStr(filterLayout.start) : "all time"} - ${filterLayout.end > 0 ? root.epochToDateStr(filterLayout.end) : "now"}` } } ListView {