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

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/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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
TransactionSigner 1.0 TransactionSigner.qml
TransactionStackView 1.0 TransactionStackView.qml
ActivityView 1.0 ActivityView.qml

2
vendor/status-go vendored

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