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
This commit is contained in:
Stefan 2023-05-11 10:56:55 +03:00 committed by Anthony Laibe
parent 1a07b73354
commit 9262943176
12 changed files with 562 additions and 124 deletions

View File

@ -1,4 +1,5 @@
import NimQml, logging, std/json, sequtils, sugar, options import NimQml, logging, std/json, sequtils, sugar, options
import tables
import model import model
import entry import entry
@ -22,6 +23,9 @@ QtObject:
model: Model model: Model
transactionsModule: transactions_module.AccessInterface transactionsModule: transactions_module.AccessInterface
currentActivityFilter: backend_activity.ActivityFilter currentActivityFilter: backend_activity.ActivityFilter
# TODO remove chains and addresses to use the app one
addresses: seq[string]
chainIds: seq[int]
proc setup(self: Controller) = proc setup(self: Controller) =
self.QObject.setup self.QObject.setup
@ -43,15 +47,15 @@ QtObject:
read = getModel read = getModel
# TODO: move it to service, make it async and lazy load details for transactions # 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 multiTransactionsIds: seq[int] = @[]
var transactionIdentities: seq[backend.TransactionIdentity] = @[] var transactionIdentities: seq[backend.TransactionIdentity] = @[]
var pendingTransactionIdentities: seq[backend.TransactionIdentity] = @[] var pendingTransactionIdentities: seq[backend.TransactionIdentity] = @[]
# Extract metadata required to fetch details # 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 # TODO: temporary here to show the working API. Will be done as required on a detail request from UI
for backendEntry in backendEnties: for backendEntry in backendEntities:
case backendEntry.transactionType: case backendEntry.payloadType:
of MultiTransaction: of MultiTransaction:
multiTransactionsIds.add(backendEntry.id) multiTransactionsIds.add(backendEntry.id)
of SimpleTransaction: of SimpleTransaction:
@ -59,11 +63,13 @@ QtObject:
of PendingTransaction: of PendingTransaction:
pendingTransactionIdentities.add(backendEntry.transaction.get()) pendingTransactionIdentities.add(backendEntry.transaction.get())
var multiTransactions: seq[MultiTransactionDto] = @[] var multiTransactions = initTable[int, MultiTransactionDto]()
if len(multiTransactionsIds) > 0: 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: if len(transactionIdentities) > 0:
let response = backend.getTransfersForIdentities(transactionIdentities) let response = backend.getTransfersForIdentities(transactionIdentities)
let res = response.result let res = response.result
@ -71,9 +77,11 @@ QtObject:
raise newException(Defect, "failed fetching transaction details") raise newException(Defect, "failed fetching transaction details")
let transactionsDtos = res.getElems().map(x => x.toTransactionDto()) 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: if len(pendingTransactionIdentities) > 0:
let response = backend.getPendingTransactionsForIdentities(pendingTransactionIdentities) let response = backend.getPendingTransactionsForIdentities(pendingTransactionIdentities)
let res = response.result let res = response.result
@ -81,33 +89,43 @@ QtObject:
raise newException(Defect, "failed fetching pending transactions details") raise newException(Defect, "failed fetching pending transactions details")
let pendingTransactionsDtos = res.getElems().map(x => x.toPendingTransactionDto()) 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 # 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 mtIndex = 0
var tIndex = 0 var tIndex = 0
var ptIndex = 0 var ptIndex = 0
for i in low(backendEnties) .. high(backendEnties): for backendEntry in backendEntities:
let backendEntry = backendEnties[i] case backendEntry.payloadType:
case backendEntry.transactionType:
of MultiTransaction: 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 mtIndex += 1
of SimpleTransaction: of SimpleTransaction:
let refInstance = new(Item) let identity = transactionIdentities[tIndex]
refInstance[] = transactions[tIndex] if transactions.hasKey(identity):
result[i] = entry.newTransactionActivityEntry(refInstance, false) result.add(entry.newTransactionActivityEntry(transactions[identity], backendEntry))
else:
error "failed to find transaction with identity: ", identity
tIndex += 1 tIndex += 1
of PendingTransaction: of PendingTransaction:
let refInstance = new(Item) let identity = pendingTransactionIdentities[ptIndex]
refInstance[] = pendingTransactions[ptIndex] if pendingTransactions.hasKey(identity):
result[i] = entry.newTransactionActivityEntry(refInstance, true) result.add(entry.newTransactionActivityEntry(pendingTransactions[identity], backendEntry))
else:
error "failed to find pending transaction with identity: ", identity
ptIndex += 1 ptIndex += 1
proc refreshData*(self: Controller) {.slot.} = proc refreshData(self: Controller) =
# result type is RpcResponse # 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 # RPC returns null for result in case of empty array
if response.error != nil or (response.result.kind != JArray and response.result.kind != JNull): if response.error != nil or (response.result.kind != JArray and response.result.kind != JNull):
error "error fetching activity entries: ", response.error error "error fetching activity entries: ", response.error
@ -117,15 +135,88 @@ QtObject:
self.model.setEntries(@[]) self.model.setEntries(@[])
return 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: for i in 0 ..< response.result.len:
backendEnties[i] = fromJson(response.result[i], backend_activity.ActivityEntry) backendEntities[i] = fromJson(response.result[i], backend_activity.ActivityEntry)
let entries = self.backendToPresentation(backendEnties) let entries = self.backendToPresentation(backendEntities)
self.model.setEntries(entries) self.model.setEntries(entries)
# TODO: add all parameters and separate in different methods proc updateFilter*(self: Controller) {.slot.} =
proc updateFilter*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} = self.refreshData()
# Update filter
proc setFilterTime*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} =
self.currentActivityFilter.period = backend_activity.newPeriod(startTimestamp, endTimestamp) 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

View File

@ -3,34 +3,44 @@ import NimQml, tables, json, strformat, sequtils, strutils, logging
import ../transactions/view import ../transactions/view
import ../transactions/item import ../transactions/item
import ./backend/transactions 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 # 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: QtObject:
type type
ActivityEntry* = ref object of QObject ActivityEntry* = ref object of QObject
# TODO: these should be removed
multi_transaction: MultiTransactionDto multi_transaction: MultiTransactionDto
transaction: ref Item transaction: ref Item
isPending: bool isPending: bool
metadata: backend.ActivityEntry
proc setup(self: ActivityEntry) = proc setup(self: ActivityEntry) =
self.QObject.setup self.QObject.setup
proc delete*(self: ActivityEntry) = proc delete*(self: ActivityEntry) =
self.QObject.delete self.QObject.delete
proc newMultiTransactionActivityEntry*(mt: MultiTransactionDto): ActivityEntry = proc newMultiTransactionActivityEntry*(mt: MultiTransactionDto, metadata: backend.ActivityEntry): ActivityEntry =
new(result, delete) new(result, delete)
result.multi_transaction = mt result.multi_transaction = mt
result.transaction = nil result.transaction = nil
result.isPending = false result.isPending = false
result.setup() result.setup()
proc newTransactionActivityEntry*(tr: ref Item, isPending: bool): ActivityEntry = proc newTransactionActivityEntry*(tr: ref Item, metadata: backend.ActivityEntry): ActivityEntry =
new(result, delete) new(result, delete)
result.multi_transaction = nil result.multi_transaction = nil
result.transaction = tr result.transaction = tr
result.isPending = isPending result.isPending = metadata.payloadType == backend.PayloadType.PendingTransaction
result.setup() result.setup()
proc isMultiTransaction*(self: ActivityEntry): bool {.slot.} = proc isMultiTransaction*(self: ActivityEntry): bool {.slot.} =
@ -125,3 +135,9 @@ QtObject:
read = getTimestamp read = getTimestamp
# TODO: properties - type, fromChains, toChains, fromAsset, toAsset, assetName # TODO: properties - type, fromChains, toChains, fromAsset, toAsset, assetName
# proc getType*(self: ActivityEntry): int {.slot.} =
# return self.metadata.activityType.int
# QtProperty[int] type:
# read = getType

View File

@ -14,7 +14,7 @@ type
fromAsset: string fromAsset: string
toAsset: string toAsset: string
fromAmount: string fromAmount: string
multiTxtype: MultiTransactionType multiTxType: MultiTransactionType
proc initMultiTransactionItem*( proc initMultiTransactionItem*(
id: int, id: int,
@ -24,7 +24,7 @@ proc initMultiTransactionItem*(
fromAsset: string, fromAsset: string,
toAsset: string, toAsset: string,
fromAmount: string, fromAmount: string,
multiTxtype: MultiTransactionType, multiTxType: MultiTransactionType,
): MultiTransactionItem = ): MultiTransactionItem =
result.id = id result.id = id
result.timestamp = timestamp result.timestamp = timestamp
@ -33,7 +33,7 @@ proc initMultiTransactionItem*(
result.fromAsset = fromAsset result.fromAsset = fromAsset
result.toAsset = toAsset result.toAsset = toAsset
result.fromAmount = fromAmount result.fromAmount = fromAmount
result.multiTxtype = multiTxtype result.multiTxType = multiTxType
proc `$`*(self: MultiTransactionItem): string = proc `$`*(self: MultiTransactionItem): string =
result = fmt"""MultiTransactionItem( result = fmt"""MultiTransactionItem(
@ -44,7 +44,7 @@ proc `$`*(self: MultiTransactionItem): string =
fromAsset: {self.fromAsset}, fromAsset: {self.fromAsset},
toAsset: {self.toAsset}, toAsset: {self.toAsset},
fromAmount: {self.fromAmount}, fromAmount: {self.fromAmount},
multiTxtype: {self.multiTxtype}, multiTxType: {self.multiTxType},
]""" ]"""
proc getId*(self: MultiTransactionItem): int = proc getId*(self: MultiTransactionItem): int =
@ -68,5 +68,5 @@ proc getToAsset*(self: MultiTransactionItem): string =
proc getFromAmount*(self: MultiTransactionItem): string = proc getFromAmount*(self: MultiTransactionItem): string =
return self.fromAmount return self.fromAmount
proc getMultiTxtype*(self: MultiTransactionItem): MultiTransactionType = proc getMultiTxType*(self: MultiTransactionItem): MultiTransactionType =
return self.multiTxtype return self.multiTxType

View File

@ -93,5 +93,5 @@ proc multiTransactionToItem*(t: MultiTransactionDto): MultiTransactionItem =
t.fromAsset, t.fromAsset,
t.toAsset, t.toAsset,
t.fromAmount, t.fromAmount,
t.multiTxtype t.multiTxType
) )

View File

@ -151,7 +151,7 @@ proc toMultiTransactionDto*(jsonObj: JsonNode): MultiTransactionDto =
discard jsonObj.getProp("fromAmount", result.fromAmount) discard jsonObj.getProp("fromAmount", result.fromAmount)
var multiTxType: int var multiTxType: int
discard jsonObj.getProp("type", multiTxType) discard jsonObj.getProp("type", multiTxType)
result.multiTxtype = cast[MultiTransactionType](multiTxType) result.multiTxType = cast[MultiTransactionType](multiTxType)
proc cmpTransactions*(x, y: TransactionDto): int = proc cmpTransactions*(x, y: TransactionDto): int =
# Sort proc to compare transactions from a single account. # Sort proc to compare transactions from a single account.

View File

@ -365,7 +365,7 @@ QtObject:
fromAsset: tokenSymbol, fromAsset: tokenSymbol,
toAsset: tokenSymbol, toAsset: tokenSymbol,
fromAmount: "0x" & amountToSend.toHex, fromAmount: "0x" & amountToSend.toHex,
multiTxtype: transactions.MultiTransactionType.MultiTransactionSend, multiTxType: transactions.MultiTransactionType.MultiTransactionSend,
), ),
paths, paths,
password, password,
@ -432,7 +432,7 @@ QtObject:
fromAsset: tokenSymbol, fromAsset: tokenSymbol,
toAsset: tokenSymbol, toAsset: tokenSymbol,
fromAmount: "0x" & amountToSend.toHex, fromAmount: "0x" & amountToSend.toHex,
multiTxtype: transactions.MultiTransactionType.MultiTransactionSend, multiTxType: transactions.MultiTransactionType.MultiTransactionSend,
), ),
paths, paths,
password, password,

View File

@ -1,13 +1,15 @@
import times, strformat import times, strformat, options
import json, json_serialization import json, json_serialization
import options import core, response_type
import ./core, ./response_type from gen import rpc
from ./gen import rpc import backend
import ./backend
import transactions import transactions
export response_type 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 common status-go types via protobuf
# TODO: consider using flags instead of list of enums # TODO: consider using flags instead of list of enums
type type
@ -27,37 +29,89 @@ type
TokenType* {.pure.} = enum TokenType* {.pure.} = enum
Asset, Collectibles 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 # see status-go/services/wallet/activity/filter.go Filter
# All empty sequences mean include all
ActivityFilter* = object ActivityFilter* = object
period* {.serializedFieldName("period").}: Period period*: Period
types* {.serializedFieldName("types").}: seq[ActivityType] types*: seq[ActivityType]
statuses* {.serializedFieldName("statuses").}: seq[ActivityStatus] statuses*: seq[ActivityStatus]
tokenTypes* {.serializedFieldName("tokenTypes").}: seq[TokenType] tokens*: Tokens
counterpartyAddresses* {.serializedFieldName("counterpartyAddresses").}: seq[string] 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 = proc newPeriod*(startTime: Option[DateTime], endTime: Option[DateTime]): Period =
if startTime.isSome: if startTime.isSome:
result.startTimestamp = startTime.get().toTime().toUnix().int result.startTimestamp = startTime.get().toTime().toUnix().int
else: else:
result.startTimestamp = 0 result.startTimestamp = noLimitTimestampForPeriod
if endTime.isSome: if endTime.isSome:
result.endTimestamp = endTime.get().toTime().toUnix().int result.endTimestamp = endTime.get().toTime().toUnix().int
else: else:
result.endTimestamp = 0 result.endTimestamp = noLimitTimestampForPeriod
proc newPeriod*(startTimestamp: int, endTimestamp: int): Period = proc newPeriod*(startTimestamp: int, endTimestamp: int): Period =
result.startTimestamp = startTimestamp result.startTimestamp = startTimestamp
result.endTimestamp = endTimestamp result.endTimestamp = endTimestamp
proc getIncludeAllActivityFilter*(): ActivityFilter = 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 # 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.period = period
result.types = activityType result.types = activityType
result.statuses = activityStatus result.statuses = activityStatus
result.tokenTypes = tokenType result.tokens = tokens
result.counterpartyAddresses = counterpartyAddress result.counterpartyAddresses = counterpartyAddress
# Mirrors status-go/services/wallet/activity/activity.go PayloadType # Mirrors status-go/services/wallet/activity/activity.go PayloadType
@ -68,8 +122,8 @@ type
PendingTransaction PendingTransaction
# Define toJson proc for PayloadType # Define toJson proc for PayloadType
proc toJson*(x: PayloadType): JsonNode {.inline.} = proc `%`*(x: PayloadType): JsonNode {.inline.} =
return %*(ord(x)) return newJInt(ord(x))
# Define fromJson proc for PayloadType # Define fromJson proc for PayloadType
proc fromJson*(x: JsonNode, T: typedesc[PayloadType]): PayloadType {.inline.} = 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 # TODO: hide internals behind safe interface
type type
ActivityEntry* = object ActivityEntry* = object
transactionType* {.serializedFieldName("transactionType").}: PayloadType # Identification
transaction* {.serializedFieldName("transaction").}: Option[TransactionIdentity] payloadType*: PayloadType
id* {.serializedFieldName("id").}: int transaction*: Option[TransactionIdentity]
timestamp* {.serializedFieldName("timestamp").}: int id*: int
activityType* {.serializedFieldName("activityType").}: MultiTransactionType
proc fromJson[T](jsonObj: JsonNode, TT: typedesc[Option[T]]): Option[T] = timestamp*: int
if jsonObj.kind != JNull: # TODO: change it into ActivityType
return some(to(jsonObj, T)) activityType*: MultiTransactionType
else: activityStatus*: ActivityStatus
return none(T) tokenType*: TokenType
proc toJson[T](obj: Option[T]): JsonNode =
if obj.isSome:
toJson(obj.get())
else:
newJNull()
# Define toJson proc for PayloadType # Define toJson proc for PayloadType
proc toJson*(ae: ActivityEntry): JsonNode {.inline.} = proc toJson*(ae: ActivityEntry): JsonNode {.inline.} =
@ -103,19 +150,24 @@ proc toJson*(ae: ActivityEntry): JsonNode {.inline.} =
# Define fromJson proc for PayloadType # Define fromJson proc for PayloadType
proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.} = proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.} =
result = T( result = T(
transactionType: fromJson(e["transactionType"], PayloadType), payloadType: fromJson(e["payloadType"], PayloadType),
transaction: if e.hasKey("transaction"): fromJson(e["transaction"], Option[TransactionIdentity]) else: none(TransactionIdentity), transaction: if e.hasKey("transaction"): fromJson(e["transaction"], Option[TransactionIdentity])
else: none(TransactionIdentity),
id: e["id"].getInt(), id: e["id"].getInt(),
timestamp: e["timestamp"].getInt() timestamp: e["timestamp"].getInt()
) )
proc `$`*(self: ActivityEntry): string = 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( return fmt"""ActivityEntry(
transactionType:{self.transactionType.int}, payloadType:{$self.payloadType},
transaction:{transactionStr}, transaction:{transactionStr},
id:{self.id}, id:{self.id},
timestamp:{self.timestamp}, timestamp:{self.timestamp},
activityType* {$self.activityType},
activityStatus* {$self.activityStatus},
tokenType* {$self.tokenType},
)""" )"""
rpc(getActivityEntries, "wallet"): rpc(getActivityEntries, "wallet"):

View File

@ -1,4 +1,5 @@
import json, json_serialization, strformat import json, json_serialization, strformat
import hashes
import ./core, ./response_type import ./core, ./response_type
from ./gen import rpc from ./gen import rpc
@ -98,9 +99,19 @@ rpc(getPendingTransactionsByChainIDs, "wallet"):
type type
TransactionIdentity* = ref object TransactionIdentity* = ref object
chainId* {.serializedFieldName("chainId").}: int chainId*: int
hash* {.serializedFieldName("hash").}: string hash*: string
address* {.serializedFieldName("address").}: 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 = proc `$`*(self: TransactionIdentity): string =
return fmt"""TransactionIdentity( return fmt"""TransactionIdentity(

View File

@ -17,7 +17,7 @@ type
fromAsset* {.serializedFieldName("fromAsset").}: string fromAsset* {.serializedFieldName("fromAsset").}: string
toAsset* {.serializedFieldName("toAsset").}: string toAsset* {.serializedFieldName("toAsset").}: string
fromAmount* {.serializedFieldName("fromAmount").}: string fromAmount* {.serializedFieldName("fromAmount").}: string
multiTxtype* {.serializedFieldName("type").}: MultiTransactionType multiTxType* {.serializedFieldName("type").}: MultiTransactionType
proc getTransactionByHash*(chainId: int, hash: string): RpcResponse[JsonNode] {.raises: [Exception].} = proc getTransactionByHash*(chainId: int, hash: string): RpcResponse[JsonNode] {.raises: [Exception].} =
core.callPrivateRPCWithChainId("eth_getTransactionByHash", chainId, %* [hash]) core.callPrivateRPCWithChainId("eth_getTransactionByHash", chainId, %* [hash])

View File

@ -84,6 +84,7 @@ Item {
text: qsTr("Activity") text: qsTr("Activity")
} }
// TODO - DEV: remove me // TODO - DEV: remove me
// Enable for debugging activity filter
// currentIndex: 3 // currentIndex: 3
// StatusTabButton { // StatusTabButton {
// rightPadding: 0 // rightPadding: 0
@ -128,6 +129,9 @@ Item {
// Layout.fillHeight: true // Layout.fillHeight: true
// controller: RootStore.activityController // controller: RootStore.activityController
// networksModel: RootStore.allNetworks
// assetsModel: RootStore.assets
// assetsLoading: RootStore.assetsLoading
// } // }
} }
} }
@ -144,7 +148,6 @@ Item {
assetsLoading: RootStore.assetsLoading assetsLoading: RootStore.assetsLoading
address: RootStore.overview.mixedcaseAddress address: RootStore.overview.mixedcaseAddress
networkConnectionStore: root.networkConnectionStore networkConnectionStore: root.networkConnectionStore
} }

View File

@ -8,6 +8,8 @@ import StatusQ.Components 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import AppLayouts.stores 1.0
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
import utils 1.0 import utils 1.0
@ -18,10 +20,134 @@ import "../stores"
import "../controls" import "../controls"
// Temporary developer view to test the filter APIs // Temporary developer view to test the filter APIs
Item { Control {
id: root 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 { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@ -29,11 +155,8 @@ Item {
ColumnLayout { ColumnLayout {
id: filterLayout id: filterLayout
readonly property int millisInADay: 24 * 60 * 60 * 1000 ColumnLayout {
property int start: fromSlider.value > 0 ? Math.floor(new Date(new Date() - (fromSlider.value * millisInADay)).getTime() / 1000) : 0 id: timeFilterLayout
property int end: toSlider.value > 0 ? Math.floor(new Date(new Date() - (toSlider.value * millisInADay)).getTime() / 1000) : 0
function updateFilter() { controller.updateFilter(start, end) }
RowLayout { RowLayout {
Label { text: "Past Days Span: 100" } Label { text: "Past Days Span: 100" }
@ -48,8 +171,6 @@ Item {
stepSize: 1 stepSize: 1
value: 0 value: 0
onPressedChanged: { if (!pressed) filterLayout.updateFilter() }
} }
Label { text: `${fromSlider.value}d - ${toSlider.value}d` } Label { text: `${fromSlider.value}d - ${toSlider.value}d` }
Slider { Slider {
@ -65,12 +186,156 @@ Item {
stepSize: 1 stepSize: 1
value: 0 value: 0
onPressedChanged: { if (!pressed) filterLayout.updateFilter() }
} }
Label { text: "0" } Label { text: "0" }
} }
Label { text: `Interval: ${filterLayout.start > 0 ? root.epochToDateStr(filterLayout.start) : "all time"} - ${filterLayout.end > 0 ? root.epochToDateStr(filterLayout.end) : "now"}` } 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 {}
}
}
} }
ListView { ListView {