mirror of
https://github.com/status-im/status-desktop.git
synced 2025-02-16 08:37:12 +00:00
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:
parent
ecc1b5316f
commit
2ba9680316
@ -1,10 +1,12 @@
|
||||
import NimQml, logging, std/json, sequtils, sugar, options
|
||||
import tables, stint
|
||||
import NimQml, logging, std/json, sequtils, sugar, options, strutils
|
||||
import tables, stint, sets
|
||||
|
||||
import model
|
||||
import entry
|
||||
import recipients_model
|
||||
|
||||
import web3/conversions
|
||||
|
||||
import ../transactions/item
|
||||
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/transaction/service as transaction_service
|
||||
import app_service/service/token/service as token_service
|
||||
|
||||
|
||||
proc toRef*[T](obj: T): ref T =
|
||||
new(result)
|
||||
@ -25,6 +29,7 @@ proc toRef*[T](obj: T): ref T =
|
||||
const FETCH_BATCH_COUNT_DEFAULT = 10
|
||||
const FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT = 2000
|
||||
|
||||
# TODO: implement passing of collectibles
|
||||
QtObject:
|
||||
type
|
||||
Controller* = ref object of QObject
|
||||
@ -33,14 +38,18 @@ QtObject:
|
||||
transactionsModule: transactions_module.AccessInterface
|
||||
currentActivityFilter: backend_activity.ActivityFilter
|
||||
currencyService: currency_service.Service
|
||||
tokenService: token_service.Service
|
||||
|
||||
events: EventEmitter
|
||||
|
||||
loadingData: bool
|
||||
errorCode: backend_activity.ErrorCode
|
||||
|
||||
# TODO remove chains and addresses after using ground truth
|
||||
# call updateAssetsIdentities after updating filterTokenCodes
|
||||
filterTokenCodes: HashSet[string]
|
||||
|
||||
addresses: seq[string]
|
||||
# call updateAssetsIdentities after updating chainIds
|
||||
chainIds: seq[int]
|
||||
|
||||
proc setup(self: Controller) =
|
||||
@ -185,20 +194,21 @@ QtObject:
|
||||
|
||||
proc updateFilter*(self: Controller) {.slot.} =
|
||||
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:
|
||||
error "error fetching activity entries: ", response.error
|
||||
self.setLoadingData(false)
|
||||
return
|
||||
|
||||
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:
|
||||
error "error fetching activity entries: ", response.error
|
||||
return
|
||||
|
||||
self.setLoadingData(true)
|
||||
|
||||
proc setFilterTime*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} =
|
||||
self.currentActivityFilter.period = backend_activity.newPeriod(startTimestamp, endTimestamp)
|
||||
|
||||
@ -214,18 +224,31 @@ QtObject:
|
||||
|
||||
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)
|
||||
result.model = newModel()
|
||||
result.recipientsModel = newRecipientsModel()
|
||||
result.transactionsModule = transactionsModule
|
||||
result.tokenService = tokenService
|
||||
result.currentActivityFilter = backend_activity.getIncludeAllActivityFilter()
|
||||
result.events = events
|
||||
result.currencyService = currencyService
|
||||
|
||||
result.loadingData = false
|
||||
result.errorCode = backend_activity.ErrorCode.ErrorCodeSuccess
|
||||
|
||||
result.filterTokenCodes = initHashSet[string]()
|
||||
|
||||
result.addresses = @[]
|
||||
result.chainIds = @[]
|
||||
|
||||
result.setup()
|
||||
|
||||
# Register and process events
|
||||
let controller = result
|
||||
|
||||
proc handleEvent(e: Args) =
|
||||
var data = WalletSignal(e)
|
||||
case data.eventType:
|
||||
@ -265,30 +288,37 @@ QtObject:
|
||||
|
||||
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)
|
||||
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())
|
||||
let tokenCode = assetsJson[i].getStr()
|
||||
self.filterTokenCodes.incl(tokenCode)
|
||||
|
||||
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
|
||||
self.updateAssetsIdentities()
|
||||
|
||||
proc setFilterAddresses*(self: Controller, addresses: seq[string]) =
|
||||
self.addresses = addresses
|
||||
@ -299,18 +329,7 @@ QtObject:
|
||||
proc setFilterChains*(self: Controller, chainIds: seq[int]) =
|
||||
self.chainIds = chainIds
|
||||
|
||||
# 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
|
||||
self.updateAssetsIdentities()
|
||||
|
||||
proc getLoadingData*(self: Controller): bool {.slot.} =
|
||||
return self.loadingData
|
||||
|
@ -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/item
|
||||
@ -7,7 +7,7 @@ import backend/activity as backend
|
||||
import ../../../shared_models/currency_amount
|
||||
|
||||
# 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.
|
||||
type
|
||||
ExtraData* = object
|
||||
@ -269,7 +269,7 @@ QtObject:
|
||||
if self.transaction == nil:
|
||||
error "getSymbol: ActivityEntry is not an transaction.Item"
|
||||
return ""
|
||||
|
||||
|
||||
if self.activityType == backend.ActivityType.Receive:
|
||||
return self.getInSymbol()
|
||||
|
||||
|
@ -101,7 +101,7 @@ proc newModule*(
|
||||
result.overviewModule = overview_module.newModule(result, events, walletAccountService, currencyService)
|
||||
result.networksModule = networks_module.newModule(result, events, networkService, walletAccountService, settingsService)
|
||||
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.view = newView(result, result.activityController)
|
||||
|
@ -4,7 +4,6 @@ import ./backend/transactions
|
||||
|
||||
const MultiTransactionMissingID* = 0
|
||||
|
||||
# TODO: make it a Qt object to be referenced in QML via ActivityView
|
||||
type
|
||||
MultiTransactionItem* = object
|
||||
id: int
|
||||
|
@ -370,7 +370,7 @@ QtObject:
|
||||
|
||||
proc getStatusToken*(self: Service): TokenDto =
|
||||
let networkDto = self.networkService.getNetworkForEns()
|
||||
return self.tokenService.findTokenBySymbol(networkDto, networkDto.sntSymbol())
|
||||
return self.tokenService.findTokenBySymbol(networkDto.chainId, networkDto.sntSymbol())
|
||||
|
||||
proc registerEns*(
|
||||
self: Service,
|
||||
|
@ -202,7 +202,7 @@ QtObject:
|
||||
proc getStatusToken*(self: Service): TokenDto =
|
||||
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] =
|
||||
let
|
||||
|
@ -153,10 +153,10 @@ QtObject:
|
||||
if self.hasContractAddressesForToken(symbol):
|
||||
return self.tokensToAddressesMap[symbol].addresses
|
||||
|
||||
proc findTokenBySymbol*(self: Service, network: NetworkDto, symbol: string): TokenDto =
|
||||
if not self.tokens.hasKey(network.chainId):
|
||||
proc findTokenBySymbol*(self: Service, chainId: int, symbol: string): TokenDto =
|
||||
if not self.tokens.hasKey(chainId):
|
||||
return
|
||||
for token in self.tokens[network.chainId]:
|
||||
for token in self.tokens[chainId]:
|
||||
if token.symbol == symbol:
|
||||
return token
|
||||
|
||||
|
@ -439,7 +439,7 @@ QtObject:
|
||||
|
||||
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 toAddress = token.address
|
||||
let transfer = Transfer(
|
||||
|
@ -2,9 +2,12 @@ import times, strformat, options
|
||||
import json, json_serialization
|
||||
import core, response_type
|
||||
import stint
|
||||
|
||||
import web3/ethtypes as eth
|
||||
import web3/conversions
|
||||
|
||||
from gen import rpc
|
||||
import backend
|
||||
import transactions
|
||||
|
||||
export response_type
|
||||
|
||||
@ -14,11 +17,9 @@ const noLimitTimestampForPeriod = 0
|
||||
# Declared in services/wallet/activity/service.go
|
||||
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
|
||||
Period* = object
|
||||
startTimestamp* : int
|
||||
startTimestamp*: int
|
||||
endTimestamp*: int
|
||||
|
||||
# see status-go/services/wallet/activity/filter.go Type
|
||||
@ -31,19 +32,19 @@ type
|
||||
|
||||
# see status-go/services/wallet/activity/filter.go TokenType
|
||||
TokenType* {.pure.} = enum
|
||||
Asset, Collectibles
|
||||
Native, Erc20, Erc721, Erc1155
|
||||
|
||||
# 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 TokenID
|
||||
TokenId* = 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]
|
||||
ChainId* = distinct int
|
||||
|
||||
# see status-go/services/wallet/activity/filter.go Token
|
||||
Token* = object
|
||||
tokenType*: TokenType
|
||||
chainId*: ChainId
|
||||
address*: Option[eth.Address]
|
||||
tokenId*: Option[TokenId]
|
||||
|
||||
# see status-go/services/wallet/activity/filter.go Filter
|
||||
# All empty sequences mean include all
|
||||
@ -51,9 +52,14 @@ type
|
||||
period*: Period
|
||||
types*: seq[ActivityType]
|
||||
statuses*: seq[ActivityStatus]
|
||||
tokens*: Tokens
|
||||
counterpartyAddresses*: seq[string]
|
||||
|
||||
# Tokens
|
||||
assets*: seq[Token]
|
||||
collectibles*: seq[Token]
|
||||
filterOutAssets*: bool
|
||||
filterOutCollectibles*: bool
|
||||
|
||||
proc toJson[T](obj: Option[T]): JsonNode =
|
||||
if obj.isSome:
|
||||
toJson(obj.get())
|
||||
@ -61,7 +67,7 @@ proc toJson[T](obj: Option[T]): JsonNode =
|
||||
newJNull()
|
||||
|
||||
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))
|
||||
else:
|
||||
return none(T)
|
||||
@ -69,30 +75,84 @@ proc fromJson[T](jsonObj: JsonNode, TT: typedesc[Option[T]]): Option[T] =
|
||||
proc `%`*(at: ActivityType): JsonNode {.inline.} =
|
||||
return newJInt(ord(at))
|
||||
|
||||
proc fromJson*(jn: JsonNode, T: typedesc[ActivityType]): ActivityType {.inline.} =
|
||||
return cast[ActivityType](jn.getInt())
|
||||
|
||||
proc `%`*(aSt: ActivityStatus): JsonNode {.inline.} =
|
||||
return newJInt(ord(aSt))
|
||||
|
||||
proc fromJson*(x: JsonNode, T: typedesc[ActivityStatus]): ActivityStatus {.inline.} =
|
||||
return cast[ActivityStatus](x.getInt())
|
||||
proc fromJson*(jn: JsonNode, T: typedesc[ActivityStatus]): ActivityStatus {.inline.} =
|
||||
return cast[ActivityStatus](jn.getInt())
|
||||
|
||||
proc `$`*(tc: TokenCode): string = $(string(tc))
|
||||
proc `$`*(ta: TokenAddress): string = $(string(ta))
|
||||
proc `%`*(tt: TokenType): JsonNode {.inline.} =
|
||||
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))
|
||||
|
||||
proc `%`*(ta: TokenAddress): JsonNode {.inline.} =
|
||||
return %(string(ta))
|
||||
proc fromJson*(jn: JsonNode, T: typedesc[TokenId]): TokenId {.inline.} =
|
||||
return cast[TokenId](jn.getStr())
|
||||
|
||||
proc parseJson*(tc: var TokenCode, node: JsonNode) =
|
||||
tc = TokenCode(node.getStr)
|
||||
proc `%`*(cid: ChainId): JsonNode {.inline.} =
|
||||
return %(int(cid))
|
||||
|
||||
proc parseJson*(ta: var TokenAddress, node: JsonNode) =
|
||||
ta = TokenAddress(node.getStr)
|
||||
proc fromJson*(jn: JsonNode, T: typedesc[ChainId]): ChainId {.inline.} =
|
||||
return cast[ChainId](jn.getInt())
|
||||
|
||||
proc newAllTokens(): Tokens =
|
||||
result.assets = none(seq[TokenCode])
|
||||
result.collectibles = none(seq[TokenAddress])
|
||||
proc `$`*(cid: ChainId): string = $(int(cid))
|
||||
|
||||
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 =
|
||||
if startTime.isSome:
|
||||
@ -109,17 +169,24 @@ proc newPeriod*(startTimestamp: int, endTimestamp: int): Period =
|
||||
result.endTimestamp = endTimestamp
|
||||
|
||||
proc getIncludeAllActivityFilter*(): ActivityFilter =
|
||||
result = ActivityFilter(period: newPeriod(none(DateTime), none(DateTime)), types: @[], statuses: @[],
|
||||
tokens: newAllTokens(), counterpartyAddresses: @[])
|
||||
result = ActivityFilter(period: newPeriod(none(DateTime), none(DateTime)),
|
||||
types: @[], statuses: @[], counterpartyAddresses: @[],
|
||||
assets: newAllTokens(), collectibles: newAllTokens(),
|
||||
filterOutAssets: false, filterOutCollectibles: false)
|
||||
|
||||
# Empty sequence for paramters means include all
|
||||
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.types = activityType
|
||||
result.statuses = activityStatus
|
||||
result.tokens = tokens
|
||||
result.counterpartyAddresses = counterpartyAddress
|
||||
result.assets = assets
|
||||
result.collectibles = collectibles
|
||||
result.filterOutAssets = filterOutAssets
|
||||
result.filterOutCollectibles = filterOutCollectibles
|
||||
|
||||
# Mirrors status-go/services/wallet/activity/activity.go PayloadType
|
||||
type
|
||||
@ -129,12 +196,12 @@ type
|
||||
PendingTransaction
|
||||
|
||||
# Define toJson proc for PayloadType
|
||||
proc `%`*(x: PayloadType): JsonNode {.inline.} =
|
||||
return newJInt(ord(x))
|
||||
proc `%`*(pt: PayloadType): JsonNode {.inline.} =
|
||||
return newJInt(ord(pt))
|
||||
|
||||
# Define fromJson proc for PayloadType
|
||||
proc fromJson*(x: JsonNode, T: typedesc[PayloadType]): PayloadType {.inline.} =
|
||||
return cast[PayloadType](x.getInt())
|
||||
proc fromJson*(jn: JsonNode, T: typedesc[PayloadType]): PayloadType {.inline.} =
|
||||
return cast[PayloadType](jn.getInt())
|
||||
|
||||
# TODO: hide internals behind safe interface
|
||||
type
|
||||
@ -145,13 +212,16 @@ type
|
||||
id*: int
|
||||
|
||||
timestamp*: int
|
||||
# TODO: change it into ActivityType
|
||||
activityType*: MultiTransactionType
|
||||
|
||||
activityType*: ActivityType
|
||||
activityStatus*: ActivityStatus
|
||||
tokenType*: TokenType
|
||||
|
||||
amountOut*: UInt256
|
||||
amountIn*: UInt256
|
||||
|
||||
tokenOut*: Option[Token]
|
||||
tokenIn*: Option[Token]
|
||||
|
||||
# Mirrors services/wallet/activity/service.go ErrorCode
|
||||
ErrorCode* = enum
|
||||
ErrorCodeSuccess = 1,
|
||||
@ -171,15 +241,30 @@ proc toJson*(ae: ActivityEntry): JsonNode {.inline.} =
|
||||
|
||||
# Define fromJson proc for PayloadType
|
||||
proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.} =
|
||||
const tokenOutField = "tokenOut"
|
||||
const tokenInField = "tokenIn"
|
||||
result = T(
|
||||
payloadType: fromJson(e["payloadType"], PayloadType),
|
||||
transaction: if e.hasKey("transaction"): fromJson(e["transaction"], Option[TransactionIdentity])
|
||||
else: none(TransactionIdentity),
|
||||
transaction: if e.hasKey("transaction"):
|
||||
fromJson(e["transaction"], Option[TransactionIdentity])
|
||||
else:
|
||||
none(TransactionIdentity),
|
||||
id: e["id"].getInt(),
|
||||
activityType: fromJson(e["activityType"], ActivityType),
|
||||
activityStatus: fromJson(e["activityStatus"], ActivityStatus),
|
||||
timestamp: e["timestamp"].getInt(),
|
||||
|
||||
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 =
|
||||
@ -192,9 +277,10 @@ proc `$`*(self: ActivityEntry): string =
|
||||
timestamp:{self.timestamp},
|
||||
activityType* {$self.activityType},
|
||||
activityStatus* {$self.activityStatus},
|
||||
tokenType* {$self.tokenType},
|
||||
amountOut* {$self.amountOut},
|
||||
amountIn* {$self.amountIn},
|
||||
tokenOut* {$self.tokenOut},
|
||||
tokenIn* {$self.tokenIn}
|
||||
)"""
|
||||
|
||||
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"):
|
||||
addresses: seq[string]
|
||||
chainIds: seq[int]
|
||||
chainIds: seq[ChainId]
|
||||
filter: ActivityFilter
|
||||
offset: int
|
||||
limit: int
|
||||
|
@ -100,7 +100,7 @@ QtObject {
|
||||
// update filters
|
||||
tokensFilter = toggleFilterState(tokensFilter, symbol, tokensList.count)
|
||||
// Set backend values
|
||||
activityController.setFilterAssets(JSON.stringify(tokensFilter))
|
||||
activityController.setFilterAssets(JSON.stringify(tokensFilter), false)
|
||||
activityController.updateFilter()
|
||||
}
|
||||
|
||||
|
@ -88,14 +88,6 @@ Item {
|
||||
width: implicitWidth
|
||||
text: qsTr("Activity")
|
||||
}
|
||||
// TODO - DEV: remove me
|
||||
// Enable for debugging activity filter
|
||||
// currentIndex: 3
|
||||
// StatusTabButton {
|
||||
// rightPadding: 0
|
||||
// width: implicitWidth
|
||||
// text: qsTr("DEV activity")
|
||||
// }
|
||||
}
|
||||
StackLayout {
|
||||
Layout.fillWidth: true
|
||||
@ -127,17 +119,6 @@ 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
|
||||
// networksModel: RootStore.allNetworks
|
||||
// assetsModel: RootStore.assets
|
||||
// assetsLoading: RootStore.assetsLoading
|
||||
// }
|
||||
}
|
||||
}
|
||||
CollectibleDetailView {
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -13,4 +13,3 @@ 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
|
||||
|
2
vendor/status-go
vendored
2
vendor/status-go
vendored
@ -1 +1 @@
|
||||
Subproject commit bf64f97d5a2bcb1b5fb6b134dda69994e0ddd2bb
|
||||
Subproject commit 8e63f447352fef5fb4eb1dbd0a87a296b2b96d78
|
Loading…
x
Reference in New Issue
Block a user