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,4 @@
# we need to link C++ libraries
gcc.linkerexe="g++"
path = "src"
path = "src"

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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.

View File

@ -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,

View File

@ -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"):

View File

@ -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(

View File

@ -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])

View File

@ -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
}

View File

@ -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 {