feat(wallet): filter activity by ERC20

Refactor code to use the token identity instead of token code
Removed the debugging activity view as now we have the API integrated
in the history view
Fixed the activity type in the activity entry

Closes: #11025
This commit is contained in:
Stefan 2023-06-13 11:25:54 +02:00 committed by Stefan Dunca
parent ecc1b5316f
commit 2ba9680316
14 changed files with 202 additions and 558 deletions

View File

@ -1,10 +1,12 @@
import NimQml, logging, std/json, sequtils, sugar, options import NimQml, logging, std/json, sequtils, sugar, options, strutils
import tables, stint import tables, stint, sets
import model import model
import entry import entry
import recipients_model import recipients_model
import web3/conversions
import ../transactions/item import ../transactions/item
import ../transactions/module as transactions_module import ../transactions/module as transactions_module
@ -17,6 +19,8 @@ import backend/transactions
import app_service/service/currency/service as currency_service import app_service/service/currency/service as currency_service
import app_service/service/transaction/service as transaction_service import app_service/service/transaction/service as transaction_service
import app_service/service/token/service as token_service
proc toRef*[T](obj: T): ref T = proc toRef*[T](obj: T): ref T =
new(result) new(result)
@ -25,6 +29,7 @@ proc toRef*[T](obj: T): ref T =
const FETCH_BATCH_COUNT_DEFAULT = 10 const FETCH_BATCH_COUNT_DEFAULT = 10
const FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT = 2000 const FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT = 2000
# TODO: implement passing of collectibles
QtObject: QtObject:
type type
Controller* = ref object of QObject Controller* = ref object of QObject
@ -33,14 +38,18 @@ QtObject:
transactionsModule: transactions_module.AccessInterface transactionsModule: transactions_module.AccessInterface
currentActivityFilter: backend_activity.ActivityFilter currentActivityFilter: backend_activity.ActivityFilter
currencyService: currency_service.Service currencyService: currency_service.Service
tokenService: token_service.Service
events: EventEmitter events: EventEmitter
loadingData: bool loadingData: bool
errorCode: backend_activity.ErrorCode errorCode: backend_activity.ErrorCode
# TODO remove chains and addresses after using ground truth # call updateAssetsIdentities after updating filterTokenCodes
filterTokenCodes: HashSet[string]
addresses: seq[string] addresses: seq[string]
# call updateAssetsIdentities after updating chainIds
chainIds: seq[int] chainIds: seq[int]
proc setup(self: Controller) = proc setup(self: Controller) =
@ -185,20 +194,21 @@ QtObject:
proc updateFilter*(self: Controller) {.slot.} = proc updateFilter*(self: Controller) {.slot.} =
self.setLoadingData(true) self.setLoadingData(true)
let response = backend_activity.filterActivityAsync(self.addresses, self.chainIds, self.currentActivityFilter, 0, FETCH_BATCH_COUNT_DEFAULT)
let response = backend_activity.filterActivityAsync(self.addresses, seq[backend_activity.ChainId](self.chainIds), self.currentActivityFilter, 0, FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil: if response.error != nil:
error "error fetching activity entries: ", response.error error "error fetching activity entries: ", response.error
self.setLoadingData(false) self.setLoadingData(false)
return return
proc loadMoreItems(self: Controller) {.slot.} = proc loadMoreItems(self: Controller) {.slot.} =
let response = backend_activity.filterActivityAsync(self.addresses, self.chainIds, self.currentActivityFilter, self.model.getCount(), FETCH_BATCH_COUNT_DEFAULT) self.setLoadingData(true)
let response = backend_activity.filterActivityAsync(self.addresses, seq[backend_activity.ChainId](self.chainIds), self.currentActivityFilter, self.model.getCount(), FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil: if response.error != nil:
error "error fetching activity entries: ", response.error error "error fetching activity entries: ", response.error
return return
self.setLoadingData(true)
proc setFilterTime*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} = proc setFilterTime*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} =
self.currentActivityFilter.period = backend_activity.newPeriod(startTimestamp, endTimestamp) self.currentActivityFilter.period = backend_activity.newPeriod(startTimestamp, endTimestamp)
@ -214,18 +224,31 @@ QtObject:
self.currentActivityFilter.types = types self.currentActivityFilter.types = types
proc newController*(transactionsModule: transactions_module.AccessInterface, events: EventEmitter, currencyService: currency_service.Service): Controller = proc newController*(transactionsModule: transactions_module.AccessInterface,
currencyService: currency_service.Service,
tokenService: token_service.Service,
events: EventEmitter): Controller =
new(result, delete) new(result, delete)
result.model = newModel() result.model = newModel()
result.recipientsModel = newRecipientsModel() result.recipientsModel = newRecipientsModel()
result.transactionsModule = transactionsModule result.transactionsModule = transactionsModule
result.tokenService = tokenService
result.currentActivityFilter = backend_activity.getIncludeAllActivityFilter() result.currentActivityFilter = backend_activity.getIncludeAllActivityFilter()
result.events = events result.events = events
result.currencyService = currencyService result.currencyService = currencyService
result.loadingData = false
result.errorCode = backend_activity.ErrorCode.ErrorCodeSuccess
result.filterTokenCodes = initHashSet[string]()
result.addresses = @[]
result.chainIds = @[]
result.setup() result.setup()
# Register and process events
let controller = result let controller = result
proc handleEvent(e: Args) = proc handleEvent(e: Args) =
var data = WalletSignal(e) var data = WalletSignal(e)
case data.eventType: case data.eventType:
@ -265,30 +288,37 @@ QtObject:
self.currentActivityFilter.counterpartyAddresses = addresses self.currentActivityFilter.counterpartyAddresses = addresses
proc setFilterAssets*(self: Controller, assetsArrayJsonString: string) {.slot.} = # Depends on self.filterTokenCodes and self.chainIds, so should be called after updating them
proc updateAssetsIdentities(self: Controller) =
var assets = newSeq[backend_activity.Token]()
for tokenCode in self.filterTokenCodes:
for chainId in self.chainIds:
let token = self.tokenService.findTokenBySymbol(chainId, tokenCode)
if token != nil:
let tokenType = if token.symbol == "ETH": backend_activity.TokenType.Native else: backend_activity.TokenType.Erc20
assets.add(backend_activity.Token(
tokenType: tokenType,
chainId: backend_activity.ChainId(token.chainId),
address: some(token.address)
))
self.currentActivityFilter.assets = assets
proc setFilterAssets*(self: Controller, assetsArrayJsonString: string, excludeAssets: bool) {.slot.} =
self.filterTokenCodes.clear()
if excludeAssets:
return
let assetsJson = parseJson(assetsArrayJsonString) let assetsJson = parseJson(assetsArrayJsonString)
if assetsJson.kind != JArray: if assetsJson.kind != JArray:
error "invalid array of json strings" error "invalid array of json strings"
return return
var assets = newSeq[TokenCode](assetsJson.len)
for i in 0 ..< assetsJson.len: for i in 0 ..< assetsJson.len:
assets[i] = TokenCode(assetsJson[i].getStr()) let tokenCode = assetsJson[i].getStr()
self.filterTokenCodes.incl(tokenCode)
self.currentActivityFilter.tokens.assets = option(assets) self.updateAssetsIdentities()
# 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
proc setFilterAddresses*(self: Controller, addresses: seq[string]) = proc setFilterAddresses*(self: Controller, addresses: seq[string]) =
self.addresses = addresses self.addresses = addresses
@ -299,18 +329,7 @@ QtObject:
proc setFilterChains*(self: Controller, chainIds: seq[int]) = proc setFilterChains*(self: Controller, chainIds: seq[int]) =
self.chainIds = chainIds self.chainIds = chainIds
# TODO: remove me and use ground truth self.updateAssetsIdentities()
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
proc getLoadingData*(self: Controller): bool {.slot.} = proc getLoadingData*(self: Controller): bool {.slot.} =
return self.loadingData return self.loadingData

View File

@ -1,4 +1,4 @@
import NimQml, tables, json, strformat, sequtils, strutils, logging, stint, strutils import NimQml, json, strformat, sequtils, strutils, logging, stint, strutils
import ../transactions/view import ../transactions/view
import ../transactions/item import ../transactions/item
@ -7,7 +7,7 @@ import backend/activity as backend
import ../../../shared_models/currency_amount import ../../../shared_models/currency_amount
# Additional data needed to build an Entry, which is # Additional data needed to build an Entry, which is
# not included in the metadata and needs to be # not included in the metadata and needs to be
# fetched from a different source. # fetched from a different source.
type type
ExtraData* = object ExtraData* = object
@ -269,7 +269,7 @@ QtObject:
if self.transaction == nil: if self.transaction == nil:
error "getSymbol: ActivityEntry is not an transaction.Item" error "getSymbol: ActivityEntry is not an transaction.Item"
return "" return ""
if self.activityType == backend.ActivityType.Receive: if self.activityType == backend.ActivityType.Receive:
return self.getInSymbol() return self.getInSymbol()

View File

@ -101,7 +101,7 @@ proc newModule*(
result.overviewModule = overview_module.newModule(result, events, walletAccountService, currencyService) result.overviewModule = overview_module.newModule(result, events, walletAccountService, currencyService)
result.networksModule = networks_module.newModule(result, events, networkService, walletAccountService, settingsService) result.networksModule = networks_module.newModule(result, events, networkService, walletAccountService, settingsService)
result.networksService = networkService result.networksService = networkService
result.activityController = activityc.newController(result.transactionsModule, events, currencyService) result.activityController = activityc.newController(result.transactionsModule, currencyService, tokenService, events)
result.filter = initFilter(result.controller, result.activityController) result.filter = initFilter(result.controller, result.activityController)
result.view = newView(result, result.activityController) result.view = newView(result, result.activityController)

View File

@ -4,7 +4,6 @@ import ./backend/transactions
const MultiTransactionMissingID* = 0 const MultiTransactionMissingID* = 0
# TODO: make it a Qt object to be referenced in QML via ActivityView
type type
MultiTransactionItem* = object MultiTransactionItem* = object
id: int id: int

View File

@ -370,7 +370,7 @@ QtObject:
proc getStatusToken*(self: Service): TokenDto = proc getStatusToken*(self: Service): TokenDto =
let networkDto = self.networkService.getNetworkForEns() let networkDto = self.networkService.getNetworkForEns()
return self.tokenService.findTokenBySymbol(networkDto, networkDto.sntSymbol()) return self.tokenService.findTokenBySymbol(networkDto.chainId, networkDto.sntSymbol())
proc registerEns*( proc registerEns*(
self: Service, self: Service,

View File

@ -202,7 +202,7 @@ QtObject:
proc getStatusToken*(self: Service): TokenDto = proc getStatusToken*(self: Service): TokenDto =
let networkDto = self.networkService.getNetworkForStickers() let networkDto = self.networkService.getNetworkForStickers()
return self.tokenService.findTokenBySymbol(networkDto, networkDto.sntSymbol()) return self.tokenService.findTokenBySymbol(networkDto.chainId, networkDto.sntSymbol())
proc buyPack*(self: Service, packId: string, address, gas, gasPrice: string, eip1559Enabled: bool, maxPriorityFeePerGas: string, maxFeePerGas: string, password: string, success: var bool): tuple[txHash: string, error: string] = proc buyPack*(self: Service, packId: string, address, gas, gasPrice: string, eip1559Enabled: bool, maxPriorityFeePerGas: string, maxFeePerGas: string, password: string, success: var bool): tuple[txHash: string, error: string] =
let let

View File

@ -153,10 +153,10 @@ QtObject:
if self.hasContractAddressesForToken(symbol): if self.hasContractAddressesForToken(symbol):
return self.tokensToAddressesMap[symbol].addresses return self.tokensToAddressesMap[symbol].addresses
proc findTokenBySymbol*(self: Service, network: NetworkDto, symbol: string): TokenDto = proc findTokenBySymbol*(self: Service, chainId: int, symbol: string): TokenDto =
if not self.tokens.hasKey(network.chainId): if not self.tokens.hasKey(chainId):
return return
for token in self.tokens[network.chainId]: for token in self.tokens[chainId]:
if token.symbol == symbol: if token.symbol == symbol:
return token return token

View File

@ -439,7 +439,7 @@ QtObject:
let network = self.networkService.getNetwork(chainID) let network = self.networkService.getNetwork(chainID)
let token = self.tokenService.findTokenBySymbol(network, tokenSymbol) let token = self.tokenService.findTokenBySymbol(network.chainId, tokenSymbol)
let amountToSend = conversion.eth2Wei(parseFloat(value), token.decimals) let amountToSend = conversion.eth2Wei(parseFloat(value), token.decimals)
let toAddress = token.address let toAddress = token.address
let transfer = Transfer( let transfer = Transfer(

View File

@ -2,9 +2,12 @@ import times, strformat, options
import json, json_serialization import json, json_serialization
import core, response_type import core, response_type
import stint import stint
import web3/ethtypes as eth
import web3/conversions
from gen import rpc from gen import rpc
import backend import backend
import transactions
export response_type export response_type
@ -14,11 +17,9 @@ const noLimitTimestampForPeriod = 0
# Declared in services/wallet/activity/service.go # Declared in services/wallet/activity/service.go
const eventActivityFilteringDone*: string = "wallet-activity-filtering-done" const eventActivityFilteringDone*: string = "wallet-activity-filtering-done"
# TODO: consider using common status-go types via protobuf
# TODO: consider using flags instead of list of enums
type type
Period* = object Period* = object
startTimestamp* : int startTimestamp*: int
endTimestamp*: int endTimestamp*: int
# see status-go/services/wallet/activity/filter.go Type # see status-go/services/wallet/activity/filter.go Type
@ -31,19 +32,19 @@ type
# see status-go/services/wallet/activity/filter.go TokenType # see status-go/services/wallet/activity/filter.go TokenType
TokenType* {.pure.} = enum TokenType* {.pure.} = enum
Asset, Collectibles Native, Erc20, Erc721, Erc1155
# see status-go/services/wallet/activity/filter.go TokenCode, TokenAddress # see status-go/services/wallet/activity/filter.go TokenID
TokenCode* = distinct string TokenId* = 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 ChainId* = distinct int
# All empty sequences or none Options mean include all
Tokens* = object # see status-go/services/wallet/activity/filter.go Token
assets*: Option[seq[TokenCode]] Token* = object
collectibles*: Option[seq[TokenAddress]] tokenType*: TokenType
enabledTypes*: seq[TokenType] chainId*: ChainId
address*: Option[eth.Address]
tokenId*: Option[TokenId]
# see status-go/services/wallet/activity/filter.go Filter # see status-go/services/wallet/activity/filter.go Filter
# All empty sequences mean include all # All empty sequences mean include all
@ -51,9 +52,14 @@ type
period*: Period period*: Period
types*: seq[ActivityType] types*: seq[ActivityType]
statuses*: seq[ActivityStatus] statuses*: seq[ActivityStatus]
tokens*: Tokens
counterpartyAddresses*: seq[string] counterpartyAddresses*: seq[string]
# Tokens
assets*: seq[Token]
collectibles*: seq[Token]
filterOutAssets*: bool
filterOutCollectibles*: bool
proc toJson[T](obj: Option[T]): JsonNode = proc toJson[T](obj: Option[T]): JsonNode =
if obj.isSome: if obj.isSome:
toJson(obj.get()) toJson(obj.get())
@ -61,7 +67,7 @@ proc toJson[T](obj: Option[T]): JsonNode =
newJNull() newJNull()
proc fromJson[T](jsonObj: JsonNode, TT: typedesc[Option[T]]): Option[T] = proc fromJson[T](jsonObj: JsonNode, TT: typedesc[Option[T]]): Option[T] =
if jsonObj.kind != JNull: if jsonObj != nil and jsonObj.kind != JNull:
return some(to(jsonObj, T)) return some(to(jsonObj, T))
else: else:
return none(T) return none(T)
@ -69,30 +75,84 @@ proc fromJson[T](jsonObj: JsonNode, TT: typedesc[Option[T]]): Option[T] =
proc `%`*(at: ActivityType): JsonNode {.inline.} = proc `%`*(at: ActivityType): JsonNode {.inline.} =
return newJInt(ord(at)) return newJInt(ord(at))
proc fromJson*(jn: JsonNode, T: typedesc[ActivityType]): ActivityType {.inline.} =
return cast[ActivityType](jn.getInt())
proc `%`*(aSt: ActivityStatus): JsonNode {.inline.} = proc `%`*(aSt: ActivityStatus): JsonNode {.inline.} =
return newJInt(ord(aSt)) return newJInt(ord(aSt))
proc fromJson*(x: JsonNode, T: typedesc[ActivityStatus]): ActivityStatus {.inline.} = proc fromJson*(jn: JsonNode, T: typedesc[ActivityStatus]): ActivityStatus {.inline.} =
return cast[ActivityStatus](x.getInt()) return cast[ActivityStatus](jn.getInt())
proc `$`*(tc: TokenCode): string = $(string(tc)) proc `%`*(tt: TokenType): JsonNode {.inline.} =
proc `$`*(ta: TokenAddress): string = $(string(ta)) return newJInt(ord(tt))
proc `%`*(tc: TokenCode): JsonNode {.inline.} = proc fromJson*(jn: JsonNode, T: typedesc[TokenType]): TokenType {.inline.} =
return cast[TokenType](jn.getInt())
proc `$`*(tc: TokenId): string = $(string(tc))
proc `%`*(tc: TokenId): JsonNode {.inline.} =
return %(string(tc)) return %(string(tc))
proc `%`*(ta: TokenAddress): JsonNode {.inline.} = proc fromJson*(jn: JsonNode, T: typedesc[TokenId]): TokenId {.inline.} =
return %(string(ta)) return cast[TokenId](jn.getStr())
proc parseJson*(tc: var TokenCode, node: JsonNode) = proc `%`*(cid: ChainId): JsonNode {.inline.} =
tc = TokenCode(node.getStr) return %(int(cid))
proc parseJson*(ta: var TokenAddress, node: JsonNode) = proc fromJson*(jn: JsonNode, T: typedesc[ChainId]): ChainId {.inline.} =
ta = TokenAddress(node.getStr) return cast[ChainId](jn.getInt())
proc newAllTokens(): Tokens = proc `$`*(cid: ChainId): string = $(int(cid))
result.assets = none(seq[TokenCode])
result.collectibles = none(seq[TokenAddress]) const addressField = "address"
const tokenIdField = "tokenId"
proc `%`*(t: Token): JsonNode {.inline.} =
result = newJObject()
result["tokenType"] = %(t.tokenType)
result["chainId"] = %(t.chainId)
if t.address.isSome:
result[addressField] = %(t.address.get)
if t.tokenId.isSome:
result[tokenIdField] = %(t.tokenId.get)
proc `%`*(t: ref Token): JsonNode {.inline.} =
return %(t[])
proc fromJson*(t: JsonNode, T: typedesc[Token]): Token {.inline.} =
result = Token()
result.tokenType = fromJson(t["tokenType"], TokenType)
result.chainId = fromJson(t["chainId"], ChainId)
if t.contains(addressField) and t[addressField].kind != JNull:
var address: eth.Address
fromJson(t[addressField], addressField, address)
result.address = some(address)
if t.contains(tokenIdField) and t[tokenIdField].kind != JNull:
result.tokenId = fromJson(t[tokenIdField], Option[TokenId])
proc fromJson*(t: JsonNode, T: typedesc[ref Token]): ref Token {.inline.} =
result = new(Token)
result[] = fromJson(t, Token)
proc `$`*(t: Token): string =
return fmt"""Token(
tokenType: {t.tokenType},
chainId*: {t.chainId},
address*: {t.address},
tokenId*: {t.tokenId}
)"""
proc `$`*(t: ref Token): string =
return $(t[])
proc newAllTokens(): seq[Token] =
return @[]
proc newPeriod*(startTime: Option[DateTime], endTime: Option[DateTime]): Period = proc newPeriod*(startTime: Option[DateTime], endTime: Option[DateTime]): Period =
if startTime.isSome: if startTime.isSome:
@ -109,17 +169,24 @@ proc newPeriod*(startTimestamp: int, endTimestamp: int): Period =
result.endTimestamp = endTimestamp result.endTimestamp = endTimestamp
proc getIncludeAllActivityFilter*(): ActivityFilter = proc getIncludeAllActivityFilter*(): ActivityFilter =
result = ActivityFilter(period: newPeriod(none(DateTime), none(DateTime)), types: @[], statuses: @[], result = ActivityFilter(period: newPeriod(none(DateTime), none(DateTime)),
tokens: newAllTokens(), counterpartyAddresses: @[]) types: @[], statuses: @[], counterpartyAddresses: @[],
assets: newAllTokens(), collectibles: newAllTokens(),
filterOutAssets: false, filterOutCollectibles: false)
# Empty sequence for paramters means include all # Empty sequence for paramters means include all
proc newActivityFilter*(period: Period, activityType: seq[ActivityType], activityStatus: seq[ActivityStatus], proc newActivityFilter*(period: Period, activityType: seq[ActivityType], activityStatus: seq[ActivityStatus],
tokens: Tokens, counterpartyAddress: seq[string]): ActivityFilter = counterpartyAddress: seq[string],
assets: seq[Token], collectibles: seq[Token],
filterOutAssets: bool, filterOutCollectibles: bool): ActivityFilter =
result.period = period result.period = period
result.types = activityType result.types = activityType
result.statuses = activityStatus result.statuses = activityStatus
result.tokens = tokens
result.counterpartyAddresses = counterpartyAddress result.counterpartyAddresses = counterpartyAddress
result.assets = assets
result.collectibles = collectibles
result.filterOutAssets = filterOutAssets
result.filterOutCollectibles = filterOutCollectibles
# Mirrors status-go/services/wallet/activity/activity.go PayloadType # Mirrors status-go/services/wallet/activity/activity.go PayloadType
type type
@ -129,12 +196,12 @@ type
PendingTransaction PendingTransaction
# Define toJson proc for PayloadType # Define toJson proc for PayloadType
proc `%`*(x: PayloadType): JsonNode {.inline.} = proc `%`*(pt: PayloadType): JsonNode {.inline.} =
return newJInt(ord(x)) return newJInt(ord(pt))
# Define fromJson proc for PayloadType # Define fromJson proc for PayloadType
proc fromJson*(x: JsonNode, T: typedesc[PayloadType]): PayloadType {.inline.} = proc fromJson*(jn: JsonNode, T: typedesc[PayloadType]): PayloadType {.inline.} =
return cast[PayloadType](x.getInt()) return cast[PayloadType](jn.getInt())
# TODO: hide internals behind safe interface # TODO: hide internals behind safe interface
type type
@ -145,13 +212,16 @@ type
id*: int id*: int
timestamp*: int timestamp*: int
# TODO: change it into ActivityType
activityType*: MultiTransactionType activityType*: ActivityType
activityStatus*: ActivityStatus activityStatus*: ActivityStatus
tokenType*: TokenType
amountOut*: UInt256 amountOut*: UInt256
amountIn*: UInt256 amountIn*: UInt256
tokenOut*: Option[Token]
tokenIn*: Option[Token]
# Mirrors services/wallet/activity/service.go ErrorCode # Mirrors services/wallet/activity/service.go ErrorCode
ErrorCode* = enum ErrorCode* = enum
ErrorCodeSuccess = 1, ErrorCodeSuccess = 1,
@ -171,15 +241,30 @@ proc toJson*(ae: ActivityEntry): JsonNode {.inline.} =
# Define fromJson proc for PayloadType # Define fromJson proc for PayloadType
proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.} = proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.} =
const tokenOutField = "tokenOut"
const tokenInField = "tokenIn"
result = T( result = T(
payloadType: fromJson(e["payloadType"], PayloadType), payloadType: fromJson(e["payloadType"], PayloadType),
transaction: if e.hasKey("transaction"): fromJson(e["transaction"], Option[TransactionIdentity]) transaction: if e.hasKey("transaction"):
else: none(TransactionIdentity), fromJson(e["transaction"], Option[TransactionIdentity])
else:
none(TransactionIdentity),
id: e["id"].getInt(), id: e["id"].getInt(),
activityType: fromJson(e["activityType"], ActivityType),
activityStatus: fromJson(e["activityStatus"], ActivityStatus), activityStatus: fromJson(e["activityStatus"], ActivityStatus),
timestamp: e["timestamp"].getInt(), timestamp: e["timestamp"].getInt(),
amountOut: stint.fromHex(UInt256, e["amountOut"].getStr()), amountOut: stint.fromHex(UInt256, e["amountOut"].getStr()),
amountIn: stint.fromHex(UInt256, e["amountIn"].getStr()) amountIn: stint.fromHex(UInt256, e["amountIn"].getStr()),
tokenOut: if e.contains(tokenOutField):
some(fromJson(e[tokenOutField], Token))
else:
none(Token),
tokenIn: if e.contains(tokenInField):
some(fromJson(e[tokenInField], Token))
else:
none(Token)
) )
proc `$`*(self: ActivityEntry): string = proc `$`*(self: ActivityEntry): string =
@ -192,9 +277,10 @@ proc `$`*(self: ActivityEntry): string =
timestamp:{self.timestamp}, timestamp:{self.timestamp},
activityType* {$self.activityType}, activityType* {$self.activityType},
activityStatus* {$self.activityStatus}, activityStatus* {$self.activityStatus},
tokenType* {$self.tokenType},
amountOut* {$self.amountOut}, amountOut* {$self.amountOut},
amountIn* {$self.amountIn}, amountIn* {$self.amountIn},
tokenOut* {$self.tokenOut},
tokenIn* {$self.tokenIn}
)""" )"""
proc fromJson*(e: JsonNode, T: typedesc[FilterResponse]): FilterResponse {.inline.} = proc fromJson*(e: JsonNode, T: typedesc[FilterResponse]): FilterResponse {.inline.} =
@ -215,7 +301,7 @@ proc fromJson*(e: JsonNode, T: typedesc[FilterResponse]): FilterResponse {.inlin
rpc(filterActivityAsync, "wallet"): rpc(filterActivityAsync, "wallet"):
addresses: seq[string] addresses: seq[string]
chainIds: seq[int] chainIds: seq[ChainId]
filter: ActivityFilter filter: ActivityFilter
offset: int offset: int
limit: int limit: int

View File

@ -100,7 +100,7 @@ QtObject {
// update filters // update filters
tokensFilter = toggleFilterState(tokensFilter, symbol, tokensList.count) tokensFilter = toggleFilterState(tokensFilter, symbol, tokensList.count)
// Set backend values // Set backend values
activityController.setFilterAssets(JSON.stringify(tokensFilter)) activityController.setFilterAssets(JSON.stringify(tokensFilter), false)
activityController.updateFilter() activityController.updateFilter()
} }

View File

@ -88,14 +88,6 @@ Item {
width: implicitWidth width: implicitWidth
text: qsTr("Activity") text: qsTr("Activity")
} }
// TODO - DEV: remove me
// Enable for debugging activity filter
// currentIndex: 3
// StatusTabButton {
// rightPadding: 0
// width: implicitWidth
// text: qsTr("DEV activity")
// }
} }
StackLayout { StackLayout {
Layout.fillWidth: true Layout.fillWidth: true
@ -127,17 +119,6 @@ Item {
stack.currentIndex = 3 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
// networksModel: RootStore.allNetworks
// assetsModel: RootStore.assets
// assetsLoading: RootStore.assetsLoading
// }
} }
} }
CollectibleDetailView { CollectibleDetailView {

View File

@ -1,440 +0,0 @@
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 AppLayouts.stores 1.0
import SortFilterProxyModel 0.2
import utils 1.0
import "../panels"
import "../popups"
import "../stores"
import "../controls"
// Temporary developer view to test the filter APIs
Control {
id: root
property var controller: null
property var networksModel: null
property var assetsModel: null
property bool assetsLoading: true
background: Rectangle {
anchors.fill: parent
color: "white"
}
Component.onCompleted: controller.updateRecipientsModel()
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))
// 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
ColumnLayout {
id: filterLayout
ColumnLayout {
id: timeFilterLayout
RowLayout {
Label { text: qsTr("Past Days Span: 100") }
Slider {
id: fromSlider
Layout.preferredWidth: 200
Layout.preferredHeight: 50
from: 100
to: 0
stepSize: 1
value: 0
}
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
}
Label { text: qsTr("0") }
}
Label { text: `Interval: ${d.start > 0 ? root.epochToDateStr(d.start) : qsTr("all time")} - ${d.end > 0 ? root.epochToDateStr(d.end) : qsTr("now")}` }
}
RowLayout {
Label { text: qsTr("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: qsTr("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 {}
}
ComboBox {
id: toAddressesComboBox
model: controller.recipientsModel
displayText: qsTr("Select TO") + (controller.recipientsModel.hasMore ? qsTr(" ...") : "")
currentIndex: -1
delegate: ItemOnOffDelegate {
textRole: "address"
}
}
Button {
text: qsTr("Update")
onClicked: d.updateFilter()
}
}
RowLayout {
Label { text: qsTr("Addresses") }
TextField {
id: addressesInput
Layout.fillWidth: true
placeholderText: qsTr("0x1234, 0x5678, ...")
}
Label { text: qsTr("Chains") }
ComboBox {
displayText: qsTr("Select chains")
Layout.preferredWidth: 300
model: clonedNetworksModel
currentIndex: -1
delegate: ItemOnOffDelegate {}
}
Label { text: qsTr("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 {
property string textRole: "text"
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[textRole] }
RowLayout {}
}
}
}
ListView {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
Component.onCompleted: {
if(controller.model.hasMore) {
controller.loadMoreItems();
}
}
model: controller.model
delegate: Item {
width: parent ? parent.width : 0
height: itemLayout.implicitHeight
readonly property var entry: model.activityEntry
ColumnLayout {
id: itemLayout
anchors.fill: parent
spacing: 5
RowLayout {
Label { text: qsTr("in"); Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.inAmount }
Label { text: qsTr("out"); Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.outAmount }
Label { text: qsTr("from"); Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.sender; Layout.maximumWidth: 200; elide: Text.ElideMiddle }
Label { text: qsTr("to"); Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.recipient; Layout.maximumWidth: 200; elide: Text.ElideMiddle }
RowLayout {} // Spacer
}
RowLayout {
Label { text: entry.isMultiTransaction ? qsTr("MT") : entry.isPendingTransaction ? qsTr("PT") : qsTr(" T") }
Label { text: `[${root.epochToDateStr(entry.timestamp)}] ` }
Label {
text: `{${
function() {
switch (entry.status) {
case Constants.TransactionStatus.Failed: return qsTr("Failed");
case Constants.TransactionStatus.Pending: return qsTr("Pending");
case Constants.TransactionStatus.Complete: return qsTr("Complete");
case Constants.TransactionStatus.Finalized: return qsTr("Finalized");
}
return qsTr("-")
}()}}`
Layout.leftMargin: 5;
}
RowLayout {} // Spacer
}
}
}
onContentYChanged: checkIfFooterVisible()
onHeightChanged: checkIfFooterVisible()
onContentHeightChanged: checkIfFooterVisible()
Connections {
target: listView.footerItem
function onHeightChanged() {
listView.checkIfFooterVisible()
}
}
function checkIfFooterVisible() {
if((contentY + height) > (contentHeight - footerItem.height) && controller.model.hasMore && !controller.loadingData) {
controller.loadMoreItems();
}
}
footer: Column {
id: loadingItems
width: listView.width
visible: controller.model.hasMore
Repeater {
model: controller.model.hasMore ? 10 : 0
Text {
text: loadingItems.loadingPattern
}
}
property string loadingPattern: ""
property int glanceOffset: 0
Timer {
interval: 25; repeat: true; running: true
onTriggered: {
let offset = loadingItems.glanceOffset
let length = 100
let slashCount = 3;
let pattern = new Array(length).fill(' ');
for (let i = 0; i < slashCount; i++) {
let position = (offset + i) % length;
pattern[position] = '/';
}
pattern = '[' + pattern.join('') + ']';
loadingItems.loadingPattern = pattern;
loadingItems.glanceOffset = (offset + 1) % length;
}
}
}
ScrollBar.vertical: ScrollBar {}
}
}
function epochToDateStr(epochTimestamp) {
var date = new Date(epochTimestamp * 1000);
return date.toLocaleString(Qt.locale(), "dd-MM-yyyy hh:mm");
}
}

View File

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

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit bf64f97d5a2bcb1b5fb6b134dda69994e0ddd2bb Subproject commit 8e63f447352fef5fb4eb1dbd0a87a296b2b96d78