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:
parent
1a07b73354
commit
9262943176
2
nim.cfg
2
nim.cfg
|
@ -1,4 +1,4 @@
|
|||
# we need to link C++ libraries
|
||||
gcc.linkerexe="g++"
|
||||
|
||||
path = "src"
|
||||
path = "src"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
# TODO: properties - type, fromChains, toChains, fromAsset, toAsset, assetName
|
||||
|
||||
# proc getType*(self: ActivityEntry): int {.slot.} =
|
||||
# return self.metadata.activityType.int
|
||||
|
||||
# QtProperty[int] type:
|
||||
# read = getType
|
|
@ -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
|
||||
proc getMultiTxType*(self: MultiTransactionItem): MultiTransactionType =
|
||||
return self.multiTxType
|
|
@ -93,5 +93,5 @@ proc multiTransactionToItem*(t: MultiTransactionDto): MultiTransactionItem =
|
|||
t.fromAsset,
|
||||
t.toAsset,
|
||||
t.fromAmount,
|
||||
t.multiTxtype
|
||||
t.multiTxType
|
||||
)
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue