feat(Wallet): add activity filter basic API

Add the possibility of retrieving the data of wallet activity based
on the given filter criteria.

Major changes:
- Bump status-go with the new equivalent API
- Add temporary developer tools
    - Debugging ActivityView QML component to test filter and display
    the activity
    - Add activity Nim package and synchronous controller and model

Considerations
- Have the model synchronous for the first iteration and then move to
  async while implementing the fetching mechanism
- Use QtObject instances in the model instead of roles over items as
  agreed with the team
- Simplify the implementation by having a simple presentation layer
  using backend (service also in the future)
- Small required fixes and improvements

Closes: #10633
Updates #10366
This commit is contained in:
Stefan 2023-04-21 12:36:24 +03:00 committed by Stefan Dunca
parent cba67b3b23
commit 22df203653
23 changed files with 700 additions and 62 deletions

View File

@ -1,3 +1,4 @@
# we need to link C++ libraries
gcc.linkerexe="g++"
path = "src"

View File

@ -0,0 +1,131 @@
import NimQml, logging, std/json, sequtils, sugar, options
import model
import entry
import ../transactions/item
import ../transactions/module as transactions_module
import backend/activity as backend_activity
import backend/backend as backend
import backend/transactions
import app_service/service/transaction/service as transaction_service
proc toRef*[T](obj: T): ref T =
new(result)
result[] = obj
QtObject:
type
Controller* = ref object of QObject
model: Model
transactionsModule: transactions_module.AccessInterface
currentActivityFilter: backend_activity.ActivityFilter
proc setup(self: Controller) =
self.QObject.setup
proc delete*(self: Controller) =
self.QObject.delete
proc newController*(transactionsModule: transactions_module.AccessInterface): Controller =
new(result, delete)
result.model = newModel()
result.transactionsModule = transactionsModule
result.currentActivityFilter = backend_activity.getIncludeAllActivityFilter()
result.setup()
proc getModel*(self: Controller): QVariant {.slot.} =
return newQVariant(self.model)
QtProperty[QVariant] model:
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] =
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:
of MultiTransaction:
multiTransactionsIds.add(backendEntry.id)
of SimpleTransaction:
transactionIdentities.add(backendEntry.transaction.get())
of PendingTransaction:
pendingTransactionIdentities.add(backendEntry.transaction.get())
var multiTransactions: seq[MultiTransactionDto] = @[]
if len(multiTransactionsIds) > 0:
multiTransactions = transaction_service.getMultiTransactions(multiTransactionsIds)
var transactions: seq[Item] = @[]
if len(transactionIdentities) > 0:
let response = backend.getTransfersForIdentities(transactionIdentities)
let res = response.result
if response.error != nil or res.kind != JArray or res.len == 0:
raise newException(Defect, "failed fetching transaction details")
let transactionsDtos = res.getElems().map(x => x.toTransactionDto())
transactions = self.transactionsModule.transactionsToItems(transactionsDtos, @[])
var pendingTransactions: seq[Item] = @[]
if len(pendingTransactionIdentities) > 0:
let response = backend.getPendingTransactionsForIdentities(pendingTransactionIdentities)
let res = response.result
if response.error != nil or res.kind != JArray or res.len == 0:
raise newException(Defect, "failed fetching pending transactions details")
let pendingTransactionsDtos = res.getElems().map(x => x.toPendingTransactionDto())
pendingTransactions = self.transactionsModule.transactionsToItems(pendingTransactionsDtos, @[])
# Merge detailed transaction info in order
result = newSeq[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:
of MultiTransaction:
result[i] = entry.newMultiTransactionActivityEntry(multiTransactions[mtIndex])
mtIndex += 1
of SimpleTransaction:
let refInstance = new(Item)
refInstance[] = transactions[tIndex]
result[i] = entry.newTransactionActivityEntry(refInstance, false)
tIndex += 1
of PendingTransaction:
let refInstance = new(Item)
refInstance[] = pendingTransactions[ptIndex]
result[i] = entry.newTransactionActivityEntry(refInstance, true)
ptIndex += 1
proc refreshData*(self: Controller) {.slot.} =
# result type is RpcResponse
let response = backend_activity.getActivityEntries(@["0x0000000000000000000000000000000000000001"], @[1], 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
return
if response.result.kind == JNull:
self.model.setEntries(@[])
return
var backendEnties = 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)
self.model.setEntries(entries)
# TODO: add all parameters and separate in different methods
proc updateFilter*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} =
# Update filter
self.currentActivityFilter.period = backend_activity.newPeriod(startTimestamp, endTimestamp)
self.refreshData()

View File

@ -0,0 +1,127 @@
import NimQml, tables, json, strformat, sequtils, strutils, logging
import ../transactions/view
import ../transactions/item
import ./backend/transactions
# 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
QtObject:
type
ActivityEntry* = ref object of QObject
multi_transaction: MultiTransactionDto
transaction: ref Item
isPending: bool
proc setup(self: ActivityEntry) =
self.QObject.setup
proc delete*(self: ActivityEntry) =
self.QObject.delete
proc newMultiTransactionActivityEntry*(mt: MultiTransactionDto): ActivityEntry =
new(result, delete)
result.multi_transaction = mt
result.transaction = nil
result.isPending = false
result.setup()
proc newTransactionActivityEntry*(tr: ref Item, isPending: bool): ActivityEntry =
new(result, delete)
result.multi_transaction = nil
result.transaction = tr
result.isPending = isPending
result.setup()
proc isMultiTransaction*(self: ActivityEntry): bool {.slot.} =
return self.multi_transaction != nil
QtProperty[bool] isMultiTransaction:
read = isMultiTransaction
proc isPendingTransaction*(self: ActivityEntry): bool {.slot.} =
return (not self.isMultiTransaction()) and self.isPending
QtProperty[bool] isPendingTransaction:
read = isPendingTransaction
proc `$`*(self: ActivityEntry): string =
let mtStr = if self.multi_transaction != nil: $(self.multi_transaction.id) else: "0"
let trStr = if self.transaction != nil: $(self.transaction[]) else: "nil"
return fmt"""ActivityEntry(
multi_transaction.id:{mtStr},
transaction:{trStr},
isPending:{self.isPending}
)"""
proc getMultiTransaction*(self: ActivityEntry): MultiTransactionDto =
if not self.isMultiTransaction():
raise newException(Defect, "ActivityEntry is not a MultiTransaction")
return self.multi_transaction
proc getTransaction*(self: ActivityEntry, pending: bool): ref Item =
if self.isMultiTransaction() or self.isPending != pending:
raise newException(Defect, "ActivityEntry is not a " & (if pending: "pending" else: "") & " Transaction")
return self.transaction
proc getSender*(self: ActivityEntry): string {.slot.} =
# TODO: lookup sender's name from addressbook and cache it or in advance
if self.isMultiTransaction():
return self.multi_transaction.fromAddress
return self.transaction[].getfrom()
QtProperty[string] sender:
read = getSender
proc getRecipient*(self: ActivityEntry): string {.slot.} =
# TODO: lookup recipient name from addressbook and cache it or in advance
if self.isMultiTransaction():
return self.multi_transaction.toAddress
return self.transaction[].getTo()
QtProperty[string] recipient:
read = getRecipient
# TODO: use CurrencyAmount?
proc getFromAmount*(self: ActivityEntry): string {.slot.} =
if self.isMultiTransaction():
return self.multi_transaction.fromAmount
error "getFromAmount: ActivityEntry is not a MultiTransaction"
return "0"
QtProperty[string] fromAmount:
read = getFromAmount
proc getToAmount*(self: ActivityEntry): string {.slot.} =
if not self.isMultiTransaction():
error "getToAmount: ActivityEntry is not a MultiTransaction"
return "0"
return self.multi_transaction.fromAmount
QtProperty[string] toAmount:
read = getToAmount
proc getAmount*(self: ActivityEntry): QVariant {.slot.} =
if not self.isMultiTransaction():
error "getAmount: ActivityEntry is not an transaction.Item"
return newQVariant(0)
return newQVariant(self.transaction[].getValue())
QtProperty[QVariant] amount:
read = getAmount
proc getTimestamp*(self: ActivityEntry): int {.slot.} =
if self.isMultiTransaction():
return self.multi_transaction.timestamp
# TODO: should we account for self.transaction[].isTimeStamp?
return self.transaction[].getTimestamp()
QtProperty[int] timestamp:
read = getTimestamp
# TODO: properties - type, fromChains, toChains, fromAsset, toAsset, assetName

View File

@ -0,0 +1,90 @@
import NimQml, Tables, strutils, strformat, sequtils
import ./entry
# TODO - DEV: remove this
import app_service/service/transaction/dto
import app/modules/shared_models/currency_amount
import ../transactions/item as transaction
type
ModelRole {.pure.} = enum
ActivityEntryRole = UserRole + 1
QtObject:
type
Model* = ref object of QAbstractListModel
entries: seq[ActivityEntry]
hasMore: bool
proc delete(self: Model) =
self.entries = @[]
self.QAbstractListModel.delete
proc setup(self: Model) =
self.QAbstractListModel.setup
proc newModel*(): Model =
new(result, delete)
result.entries = @[]
result.setup
result.hasMore = true
proc `$`*(self: Model): string =
for i in 0 ..< self.entries.len:
result &= fmt"""[{i}]:({$self.entries[i]})"""
proc countChanged(self: Model) {.signal.}
proc getCount*(self: Model): int {.slot.} =
self.entries.len
QtProperty[int] count:
read = getCount
notify = countChanged
method rowCount(self: Model, index: QModelIndex = nil): int =
return self.entries.len
method roleNames(self: Model): Table[int, string] =
{
ModelRole.ActivityEntryRole.int:"activityEntry",
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
if (not index.isValid):
return
if (index.row < 0 or index.row >= self.entries.len):
return
let entry = self.entries[index.row]
let enumRole = role.ModelRole
case enumRole:
of ModelRole.ActivityEntryRole:
result = newQVariant(entry)
proc setEntries*(self: Model, entries: seq[ActivityEntry]) =
self.beginResetModel()
self.entries = entries
self.endResetModel()
self.countChanged()
# TODO: update data
# TODO: fetch more
proc hasMoreChanged*(self: Model) {.signal.}
proc getHasMore*(self: Model): bool {.slot.} =
return self.hasMore
proc setHasMore*(self: Model, hasMore: bool) {.slot.} =
self.hasMore = hasMore
self.hasMoreChanged()
QtProperty[bool] hasMore:
read = getHasMore
write = setHasMore
notify = hasMoreChanged

View File

@ -15,7 +15,7 @@ type
walletAccountService: wallet_account_service.Service
currencyService: currency_service.Service
networkService: network_service.Service
proc newController*(
delegate: io_interface.AccessInterface,
settingsService: settings_service.Service,

View File

@ -16,6 +16,8 @@ import ./networks/module as networks_module
import ./overview/module as overview_module
import ./send/module as send_module
import ./activity/controller as activityc
import ../../../global/global_singleton
import ../../../core/eventemitter
import ../../../../app_service/service/keycard/service as keycard_service
@ -34,7 +36,6 @@ import ../../../../app_service/service/network_connection/service as network_con
logScope:
topics = "wallet-section-module"
import io_interface
export io_interface
type
@ -42,7 +43,7 @@ type
delegate: delegate_interface.AccessInterface
events: EventEmitter
moduleLoaded: bool
controller: Controller
controller: controller.Controller
view: View
filter: Filter
@ -61,6 +62,8 @@ type
accountsService: accounts_service.Service
walletAccountService: wallet_account_service.Service
activityController: activityc.Controller
proc newModule*(
delegate: delegate_interface.AccessInterface,
events: EventEmitter,
@ -85,7 +88,6 @@ proc newModule*(
result.walletAccountService = walletAccountService
result.moduleLoaded = false
result.controller = newController(result, settingsService, walletAccountService, currencyService, networkService)
result.view = newView(result)
result.accountsModule = accounts_module.newModule(result, events, walletAccountService, networkService, currencyService)
result.allTokensModule = all_tokens_module.newModule(result, events, tokenService, walletAccountService)
@ -99,6 +101,10 @@ proc newModule*(
result.networksModule = networks_module.newModule(result, events, networkService, walletAccountService, settingsService)
result.filter = initFilter(result.controller)
result.activityController = activityc.newController(result.transactionsModule)
result.view = newView(result, result.activityController)
method delete*(self: Module) =
self.accountsModule.delete
self.allTokensModule.delete
@ -110,6 +116,7 @@ method delete*(self: Module) =
self.sendModule.delete
self.controller.delete
self.view.delete
self.activityController.delete
if not self.addAccountModule.isNil:
self.addAccountModule.delete

View File

@ -9,6 +9,7 @@ import ../../../shared_modules/keycard_popup/io_interface as keycard_shared_modu
import ../../../../core/[main]
import ../../../../core/tasks/[qt, threadpool]
import ./backend/transactions
type
Controller* = ref object of RootObj
@ -100,7 +101,7 @@ proc getWalletAccountByAddress*(self: Controller, address: string): WalletAccoun
proc loadTransactions*(self: Controller, address: string, toBlock: Uint256, limit: int = 20, loadMore: bool = false) =
self.transactionService.loadTransactions(address, toBlock, limit, loadMore)
proc getAllTransactions*(self: Controller, address: string): seq[TransactionDto] =
proc getAllTransactions*(self: Controller, address: string): seq[TransactionDto] =
return self.transactionService.getAllTransactions(address)
proc getChainIdForChat*(self: Controller): int =
@ -122,4 +123,4 @@ proc findTokenSymbolByAddress*(self: Controller, address: string): string =
return self.walletAccountService.findTokenSymbolByAddress(address)
proc getMultiTransactions*(self: Controller, transactionIDs: seq[int]): seq[MultiTransactionDto] =
return self.transactionService.getMultiTransactions(transactionIDs)
return transaction_service.getMultiTransactions(transactionIDs)

View File

@ -3,6 +3,8 @@ import ../../../../../app_service/service/collectible/dto as CollectibleDto
import ../../../../../app_service/service/transaction/dto
export TransactionDto, CollectibleDto
import ./item
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
## Abstract class for any input/interaction with this module.
@ -43,13 +45,13 @@ method setIsNonArchivalNode*(self: AccessInterface, isNonArchivalNode: bool) {.b
method transactionWasSent*(self: AccessInterface, result: string) {.base.} =
raise newException(ValueError, "No implementation available")
method getChainIdForChat*(self: AccessInterface): int =
method getChainIdForChat*(self: AccessInterface): int {.base.} =
raise newException(ValueError, "No implementation available")
method getChainIdForBrowser*(self: AccessInterface): int =
method getChainIdForBrowser*(self: AccessInterface): int {.base.} =
raise newException(ValueError, "No implementation available")
method refreshTransactions*(self: AccessInterface) {.base.} =
method refreshTransactions*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
# View Delegate Interface
@ -60,3 +62,6 @@ method viewDidLoad*(self: AccessInterface) {.base.} =
method getLastTxBlockNumber*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method transactionsToItems*(self: AccessInterface, transactions: seq[TransactionDto], collectibles: seq[CollectibleDto]): seq[Item] {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -240,6 +240,7 @@ proc getTxStatus*(self: Item): string =
proc getValue*(self: Item): CurrencyAmount =
return self.value
# TODO: fix naming
proc getfrom*(self: Item): string =
return self.fro

View File

@ -59,26 +59,10 @@ proc getResolvedSymbol*(self: Module, transaction: TransactionDto): string =
else:
result = "ETH"
proc transactionsToItems(self: Module, transactions: seq[TransactionDto], collectibles: seq[CollectibleDto]) : seq[Item] =
method transactionsToItems*(self: Module, transactions: seq[TransactionDto], collectibles: seq[CollectibleDto]): seq[Item] =
let gweiFormat = self.controller.getCurrencyFormat("Gwei")
let ethFormat = self.controller.getCurrencyFormat("ETH")
# TODO: Continue merging multi-transactions with transactions
#
# let transactionIDs = transactions.filter(t => t.multiTransactionID != MultiTransactionMissingID).map(t => t.multiTransactionID)
# let multiTransactions = self.controller.getMultiTransactions(transactionIDs)
# for mt in multiTransactions:
# let mtItem = multiTransactionToItem(mt)
#
# Tip: depending of the new design best will be to replace the transaction.View
# with a new ActivityEntry that contains eighter a transaction or a multi-transaction
# Refactor transaction Model to serve ActivityEntry istead of Views
#
# Here we should filter all transactions that are part of a multi-transaciton
# and add them to the multi-transaction View associated with an ActivityEntry
# and the remaining "free" transactions to the corresponding ActivityEntry
# TODO: check TransactionsItem changes
transactions.map(t => (block:
if t.typeValue == ERC721_TRANSACTION_TYPE:
for c in collectibles:
@ -138,7 +122,7 @@ method setHistoryFetchState*(self: Module, addresses: seq[string], isFetching: b
method setHistoryFetchState*(self: Module, address: string, allTxLoaded: bool, isFetching: bool) =
self.view.setHistoryFetchState(address, allTxLoaded, isFetching)
method setIsNonArchivalNode*(self: Module, isNonArchivalNode: bool) =
self.view.setIsNonArchivalNode(isNonArchivalNode)

View File

@ -1,9 +1,10 @@
import strformat
import ../../../../../app_service/service/transaction/dto
import ./backend/transactions
const MultiTransactionMissingID* = 0
# TODO: make it a Qt object to be referenced in QML via ActivityView
type
MultiTransactionItem* = object
id: int

View File

@ -10,6 +10,8 @@ import ../../../shared_models/currency_amount
import ./item
import ./multi_transaction_item
import ./backend/transactions
proc hex2GweiCurrencyAmount(hexValueStr: string, gweiFormat: CurrencyFormatDto): CurrencyAmount =
let value = parseFloat(singletonInstance.utils.hex2Gwei(hexValueStr))
return currencyAmountToItem(value, gweiFormat)

View File

@ -83,7 +83,7 @@ QtObject:
for tx in transactions:
if not self.enabledChainIds.contains(tx.getChainId()):
continue
toAddTransactions.add(tx)
if not self.models.hasKey(address):

View File

@ -1,5 +1,6 @@
import NimQml, json
import ./activity/controller as activityc
import ./io_interface
import ../../shared_models/currency_amount
@ -13,6 +14,7 @@ QtObject:
isMnemonicBackedUp: bool
tmpAmount: float # shouldn't be used anywhere except in prepare*/getPrepared* procs
tmpSymbol: string # shouldn't be used anywhere except in prepare*/getPrepared* procs
activityController: activityc.Controller
proc setup(self: View) =
self.QObject.setup
@ -20,9 +22,10 @@ QtObject:
proc delete*(self: View) =
self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View =
proc newView*(delegate: io_interface.AccessInterface, activityController: activityc.Controller): View =
new(result, delete)
result.delegate = delegate
result.activityController = activityController
result.setup()
proc load*(self: View) =
@ -120,4 +123,9 @@ QtObject:
proc destroyAddAccountPopup*(self: View) {.signal.}
proc emitDestroyAddAccountPopup*(self: View) =
self.destroyAddAccountPopup()
self.destroyAddAccountPopup()
proc getActivityController(self: View): QVariant {.slot.} =
return newQVariant(self.activityController)
QtProperty[QVariant] activityController:
read = getActivityController

View File

@ -1,12 +1,14 @@
import json, strutils, stint, json_serialization, strformat
import
web3/ethtypes, json_serialization
web3/ethtypes
include ../../common/json_utils
import ../network/dto
import ../../common/conversion as service_conversion
import ./backend/transactions
type
PendingTransactionTypeDto* {.pure.} = enum
RegisterENS = "RegisterENS",
@ -20,20 +22,6 @@ type
proc event*(self:PendingTransactionTypeDto):string =
result = "transaction:" & $self
type
MultiTransactionType* = enum
MultiTransactionSend = 0, MultiTransactionSwap = 1, MultiTransactionBridge = 2
type MultiTransactionDto* = ref object of RootObj
id* {.serializedFieldName("id").}: int
timestamp* {.serializedFieldName("timestamp").}: int
fromAddress* {.serializedFieldName("fromAddress").}: string
toAddress* {.serializedFieldName("toAddress").}: string
fromAsset* {.serializedFieldName("fromAsset").}: string
toAsset* {.serializedFieldName("toAsset").}: string
fromAmount* {.serializedFieldName("fromAmount").}: string
multiTxtype* {.serializedFieldName("type").}: MultiTransactionType
type
TransactionDto* = ref object of RootObj
id*: string

View File

@ -18,7 +18,7 @@ import ../token/service as token_service
import ../settings/service as settings_service
import ../collectible/dto
import ../eth/dto/transaction as transaction_data_dto
import ../eth/dto/[method_dto, coder, method_dto]
import ../eth/dto/[coder, method_dto]
import ./dto as transaction_dto
import ./cryptoRampDto
import ../eth/utils as eth_utils
@ -158,16 +158,6 @@ QtObject:
error "error: ", errDescription
return
proc getMultiTransactions*(self: Service, transactionIDs: seq[int]): seq[MultiTransactionDto] =
try:
let response = transactions.getMultiTransactions(transactionIDs).result
return response.getElems().map(x => x.toMultiTransactionDto())
except Exception as e:
let errDescription = e.msg
error "error: ", errDescription
return
proc getAllTransactions*(self: Service, address: string): seq[TransactionDto] =
if not self.allTransactions.hasKey(address):
return @[]
@ -375,7 +365,7 @@ QtObject:
fromAsset: tokenSymbol,
toAsset: tokenSymbol,
fromAmount: "0x" & amountToSend.toHex,
multiTxtype: MultiTransactionType.MultiTransactionSend,
multiTxtype: transactions.MultiTransactionType.MultiTransactionSend,
),
paths,
password,
@ -442,7 +432,7 @@ QtObject:
fromAsset: tokenSymbol,
toAsset: tokenSymbol,
fromAmount: "0x" & amountToSend.toHex,
multiTxtype: MultiTransactionType.MultiTransactionSend,
multiTxtype: transactions.MultiTransactionType.MultiTransactionSend,
),
paths,
password,
@ -547,3 +537,13 @@ QtObject:
except Exception as e:
error "Error getting latest block number", message = e.msg
return ""
proc getMultiTransactions*(transactionIDs: seq[int]): seq[MultiTransactionDto] =
try:
let response = transactions.getMultiTransactions(transactionIDs).result
return response.getElems().map(x => x.toMultiTransactionDto())
except Exception as e:
let errDescription = e.msg
error "error: ", errDescription
return

126
src/backend/activity.nim Normal file
View File

@ -0,0 +1,126 @@
import times, strformat
import json, json_serialization
import options
import ./core, ./response_type
from ./gen import rpc
import ./backend
import transactions
export response_type
# TODO: consider using common status-go types via protobuf
# TODO: consider using flags instead of list of enums
type
Period* = object
startTimestamp*: int
endTimestamp*: int
# see status-go/services/wallet/activity/filter.go Type
ActivityType* {.pure.} = enum
Send, Receive, Buy, Swap, Bridge
# see status-go/services/wallet/activity/filter.go Status
ActivityStatus* {.pure.} = enum
Failed, Pending, Complete, Finalized
# see status-go/services/wallet/activity/filter.go TokenType
TokenType* {.pure.} = enum
Asset, Collectibles
# see status-go/services/wallet/activity/filter.go Filter
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]
proc newPeriod*(startTime: Option[DateTime], endTime: Option[DateTime]): Period =
if startTime.isSome:
result.startTimestamp = startTime.get().toTime().toUnix().int
else:
result.startTimestamp = 0
if endTime.isSome:
result.endTimestamp = endTime.get().toTime().toUnix().int
else:
result.endTimestamp = 0
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: @[])
# Empty sequence for paramters means include all
proc newActivityFilter*(period: Period, activityType: seq[ActivityType], activityStatus: seq[ActivityStatus], tokenType: seq[TokenType], counterpartyAddress: seq[string]): ActivityFilter =
result.period = period
result.types = activityType
result.statuses = activityStatus
result.tokenTypes = tokenType
result.counterpartyAddresses = counterpartyAddress
# Mirrors status-go/services/wallet/activity/activity.go PayloadType
type
PayloadType* {.pure.} = enum
MultiTransaction = 1
SimpleTransaction
PendingTransaction
# Define toJson proc for PayloadType
proc toJson*(x: PayloadType): JsonNode {.inline.} =
return %*(ord(x))
# Define fromJson proc for PayloadType
proc fromJson*(x: JsonNode, T: typedesc[PayloadType]): PayloadType {.inline.} =
return cast[PayloadType](x.getInt())
# 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
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()
# Define toJson proc for PayloadType
proc toJson*(ae: ActivityEntry): JsonNode {.inline.} =
return %*(ae)
# 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),
id: e["id"].getInt(),
timestamp: e["timestamp"].getInt()
)
proc `$`*(self: ActivityEntry): string =
let transactionStr = if self.transaction.isSome: $self.transaction.get() else: "none(TransactionIdentity)"
return fmt"""ActivityEntry(
transactionType:{self.transactionType.int},
transaction:{transactionStr},
id:{self.id},
timestamp:{self.timestamp},
)"""
rpc(getActivityEntries, "wallet"):
addresses: seq[string]
chainIds: seq[int]
filter: ActivityFilter
offset: int
limit: int

View File

@ -1,4 +1,4 @@
import json, json_serialization
import json, json_serialization, strformat
import ./core, ./response_type
from ./gen import rpc
@ -96,6 +96,25 @@ rpc(getTokensBalancesForChainIDs, "wallet"):
rpc(getPendingTransactionsByChainIDs, "wallet"):
chainIds: seq[int]
type
TransactionIdentity* = ref object
chainId* {.serializedFieldName("chainId").}: int
hash* {.serializedFieldName("hash").}: string
address* {.serializedFieldName("address").}: string
proc `$`*(self: TransactionIdentity): string =
return fmt"""TransactionIdentity(
chainId:{self.chainId},
hash:{self.hash},
address:{self.address},
)"""
rpc(getPendingTransactionsForIdentities, "wallet"):
identities = seq[TransactionIdentity]
rpc(getTransfersForIdentities, "wallet"):
identities = seq[TransactionIdentity]
rpc(getWalletToken, "wallet"):
accounts: seq[string]

View File

@ -1,10 +1,27 @@
import json, stint
import json, stint, json_serialization
import ../app_service/service/transaction/dto
import ../app_service/service/eth/dto/transaction
import ./core as core
import ../app_service/common/utils
# mirrors the MultiTransactionType from status-go, services/wallet/transfer/transaction.go
type
MultiTransactionType* = enum
MultiTransactionSend = 0, MultiTransactionSwap = 1, MultiTransactionBridge = 2
MultiTransactionDto* = ref object of RootObj
id* {.serializedFieldName("id").}: int
timestamp* {.serializedFieldName("timestamp").}: int
fromAddress* {.serializedFieldName("fromAddress").}: string
toAddress* {.serializedFieldName("toAddress").}: string
fromAsset* {.serializedFieldName("fromAsset").}: string
toAsset* {.serializedFieldName("toAsset").}: string
fromAmount* {.serializedFieldName("fromAmount").}: string
multiTxtype* {.serializedFieldName("type").}: MultiTransactionType
proc getTransactionByHash*(chainId: int, hash: string): RpcResponse[JsonNode] {.raises: [Exception].} =
core.callPrivateRPCWithChainId("eth_getTransactionByHash", chainId, %* [hash])
proc checkRecentHistory*(chainIds: seq[int], addresses: seq[string]) {.raises: [Exception].} =
let payload = %* [chainIds, addresses]
discard core.callPrivateRPC("wallet_checkRecentHistoryForChainIDs", payload)

View File

@ -29,6 +29,7 @@ QtObject {
property var walletSectionInst: walletSection
property var totalCurrencyBalance: walletSection.totalCurrencyBalance
property var activityController: walletSection.activityController
property string signingPhrase: walletSection.signingPhrase
property string mnemonicBackedUp: walletSection.isMnemonicBackedUp

View File

@ -83,6 +83,13 @@ Item {
width: implicitWidth
text: qsTr("Activity")
}
// TODO - DEV: remove me
// currentIndex: 3
// StatusTabButton {
// rightPadding: 0
// width: implicitWidth
// text: qsTr("DEV activity")
// }
}
StackLayout {
Layout.fillWidth: true
@ -114,6 +121,14 @@ Item {
stack.currentIndex = 3
}
}
// TODO: replace with the real activity view
// Enable for debugging activity filter
// ActivityView {
// Layout.fillWidth: true
// Layout.fillHeight: true
// controller: RootStore.activityController
// }
}
}
CollectibleDetailView {

View File

@ -0,0 +1,113 @@
import QtQuick 2.15
import QtQml 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import SortFilterProxyModel 0.2
import utils 1.0
import "../panels"
import "../popups"
import "../stores"
import "../controls"
// Temporary developer view to test the filter APIs
Item {
id: root
property var controller
ColumnLayout {
anchors.fill: parent
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
function updateFilter() { controller.updateFilter(start, end) }
RowLayout {
Label { text: "Past Days Span: 100" }
Slider {
id: fromSlider
Layout.preferredWidth: 200
Layout.preferredHeight: 50
from: 100
to: 0
stepSize: 1
value: 0
onPressedChanged: { if (!pressed) filterLayout.updateFilter() }
}
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 {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
model: controller.model
delegate: Item {
width: parent ? parent.width : 0
height: itemLayout.implicitHeight
readonly property var entry: model.activityEntry
RowLayout {
id: itemLayout
anchors.fill: parent
Label { text: entry.isMultiTransaction ? "MT" : entry.isPendingTransaction ? "PT" : " T" }
Label { text: `[${root.epochToDateStr(entry.timestamp)}] ` }
Label { text: entry.isMultiTransaction ? entry.fromAmount : entry.amount }
Label { text: "from"; Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.sender; Layout.maximumWidth: 200; elide: Text.ElideMiddle }
Label { text: "to"; Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.recipient; Layout.maximumWidth: 200; elide: Text.ElideMiddle }
Label { text: "got"; Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction }
Label { text: entry.toAmount; Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction }
RowLayout {} // Spacer
}
}
}
}
function epochToDateStr(epochTimestamp) {
var date = new Date(epochTimestamp * 1000);
return date.toLocaleString(Qt.locale(), "dd-MM-yyyy hh:mm");
}
}

View File

@ -13,3 +13,4 @@ TokenListView 1.0 TokenListView.qml
TransactionPreview 1.0 TransactionPreview.qml
TransactionSigner 1.0 TransactionSigner.qml
TransactionStackView 1.0 TransactionStackView.qml
ActivityView 1.0 ActivityView.qml