feat(@desktop/wallet): Transaction collectibles filtering (#12162)

This commit is contained in:
Cuteivist 2023-09-21 08:58:44 +02:00 committed by GitHub
parent 521be27ae0
commit fddcc3a83f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 142 additions and 39 deletions

View File

@ -30,6 +30,9 @@ proc toRef*[T](obj: T): ref T =
const FETCH_BATCH_COUNT_DEFAULT = 10
const FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT = 2000
type
CollectiblesToTokenConverter* = proc (id: string): backend_activity.Token
QtObject:
type
Controller* = ref object of QObject
@ -54,6 +57,8 @@ QtObject:
requestId: int32
collectiblesToTokenConverter: CollectiblesToTokenConverter
proc setup(self: Controller) =
self.QObject.setup
@ -236,7 +241,8 @@ QtObject:
proc newController*(requestId: int32,
currencyService: currency_service.Service,
tokenService: token_service.Service,
events: EventEmitter): Controller =
events: EventEmitter,
collectiblesConverter: CollectiblesToTokenConverter): Controller =
new(result, delete)
result.requestId = requestId
@ -256,6 +262,8 @@ QtObject:
result.allAddressesSelected = false
result.chainIds = @[]
result.collectiblesToTokenConverter = collectiblesConverter
result.setup()
result.setupEventHandlers()
@ -284,6 +292,20 @@ QtObject:
self.currentActivityFilter.counterpartyAddresses = addresses
proc setFilterCollectibles*(self: Controller, collectiblesArrayJsonString: string) {.slot.} =
let collectiblesJson = parseJson(collectiblesArrayJsonString)
if collectiblesJson.kind != JArray:
error "invalid array of json strings"
return
var collectibles = newSeq[backend_activity.Token]()
for i in 0 ..< collectiblesJson.len:
let uid = collectiblesJson[i].getStr()
let token = self.collectiblesToTokenConverter(uid)
collectibles.add(token)
self.currentActivityFilter.collectibles = collectibles
# Depends on self.filterTokenCodes and self.chainIds, so should be called after updating them
proc updateAssetsIdentities(self: Controller) =
var assets = newSeq[backend_activity.Token]()

View File

@ -206,7 +206,7 @@ QtObject:
if self.metadata.tokenIn.isSome:
let address = self.metadata.tokenIn.unsafeGet().address
if address.isSome:
return toHex(address.unsafeGet())
return "0x" & toHex(address.unsafeGet())
return ""
QtProperty[string] tokenInAddress:
@ -216,7 +216,7 @@ QtObject:
if self.metadata.tokenOut.isSome:
let address = self.metadata.tokenOut.unsafeGet().address
if address.isSome:
return toHex(address.unsafeGet())
return "0x" & toHex(address.unsafeGet())
return ""
QtProperty[string] tokenOutAddress:

View File

@ -35,6 +35,8 @@ import app_service/service/network_connection/service as network_connection_serv
import app_service/service/devices/service as devices_service
import backend/collectibles as backend_collectibles
import backend/activity as backend_activity
logScope:
topics = "wallet-section-module"
@ -120,13 +122,16 @@ proc newModule*(
result.networksService = networkService
result.transactionService = transactionService
result.activityController = activityc.newController(int32(ActivityID.History), currencyService, tokenService, events)
result.tmpActivityController = activityc.newController(int32(ActivityID.Temporary), currencyService, tokenService, events)
result.collectiblesController = collectiblesc.newController(
let collectiblesController = collectiblesc.newController(
requestId = int32(backend_collectibles.CollectiblesRequestID.WalletAccount),
autofetch = false,
events = events
)
result.collectiblesController = collectiblesController
let collectiblesToTokenConverter = proc(id: string): backend_activity.Token =
return collectiblesController.getActivityToken(id)
result.activityController = activityc.newController(int32(ActivityID.History), currencyService, tokenService, events, collectiblesToTokenConverter)
result.tmpActivityController = activityc.newController(int32(ActivityID.Temporary), currencyService, tokenService, events, collectiblesToTokenConverter)
result.collectibleDetailsController = collectible_detailsc.newController(int32(backend_collectibles.CollectiblesRequestID.WalletAccount), networkService, events)
result.filter = initFilter(result.controller)

View File

@ -2,6 +2,8 @@ import NimQml, Tables, strutils, strformat, sequtils, stint
import logging
import ./collectibles_item, ./collectible_trait_model
import web3/ethtypes as eth
import backend/activity as backend_activity
type
CollectibleRole* {.pure.} = enum
@ -323,3 +325,19 @@ QtObject:
if(cmpIgnoreCase(item.getId(), id) == 0):
return item.getName()
return ""
proc getActivityToken*(self: Model, id: string): backend_activity.Token =
for item in self.items:
if(cmpIgnoreCase(item.getId(), id) == 0):
result.tokenType = backend_activity.TokenType.Erc721
result.chainId = backend_activity.ChainId(item.getChainId())
var contract = item.getContractAddress()
if len(contract) > 0:
var address: eth.Address
address = eth.fromHex(eth.Address, contract)
result.address = some(address)
var tokenId = item.getTokenId()
if tokenId > 0:
result.tokenId = some(backend_activity.TokenId("0x" & stint.toHex(tokenId)))
return result
return result

View File

@ -8,6 +8,7 @@ import events_handler
import app/core/eventemitter
import backend/collectibles as backend_collectibles
import backend/activity as backend_activity
const FETCH_BATCH_COUNT_DEFAULT = 50
@ -123,7 +124,7 @@ QtObject:
let res = fromJson(response, backend_collectibles.FilterOwnedCollectiblesResponse)
let isError = res.errorCode != ErrorCodeSuccess
let isError = res.errorCode != backend_collectibles.ErrorCodeSuccess
if isError:
error "error fetching collectibles entries: ", res.errorCode
@ -203,3 +204,6 @@ QtObject:
self.eventsHandler.updateSubscribedChainIDs(self.chainIds)
self.resetModel()
proc getActivityToken*(self: Controller, id: string): backend_activity.Token =
return self.model.getActivityToken(id)

View File

@ -25,6 +25,10 @@ Column {
activityFilterStore.updateFilterBase()
}
function resetView() {
activityFilterMenu.resetView()
}
Flow {
width: parent.width
@ -157,10 +161,27 @@ Column {
Repeater {
model: activityFilterStore.collectiblesFilter
delegate: ActivityFilterTagItem {
tagPrimaryLabel.text: activityFilterStore.collectiblesList.getName(modelData)
iconAsset.icon: activityFilterStore.collectiblesList.getImageUrl(modelData)
id: collectibleTag
property string uid: modelData
readonly property bool isValid: tagPrimaryLabel.text.length > 0
tagPrimaryLabel.text: activityFilterStore.collectiblesList.getName(uid)
iconAsset.icon: activityFilterStore.collectiblesList.getImageUrl(uid)
iconAsset.color: "transparent"
onClosed: activityFilterStore.toggleCollectibles(model.id)
onClosed: activityFilterStore.toggleCollectibles(uid)
Connections {
// Collectibles model is fetched asynchronousl, so data might not be available
target: activityFilterStore.collectiblesList
enabled: !collectibleTag.isValid
function onIsFetchingChanged() {
if (activityFilterStore.collectiblesList.isFetching)
return
collectibleTag.uid = ""
collectibleTag.uid = modelData
if (!collectibleTag.isValid)
activityFilterStore.collectiblesList.loadMore()
}
}
}
}
@ -228,7 +249,7 @@ Column {
collectiblesList: activityFilterStore.collectiblesList
collectiblesFilter: activityFilterStore.collectiblesFilter
onUpdateTokensFilter: activityFilterStore.toggleToken(tokenSymbol)
onUpdateCollectiblesFilter: activityFilterStore.toggleCollectibles(id)
onUpdateCollectiblesFilter: activityFilterStore.toggleCollectibles(uid)
store: root.store
recentsList: activityFilterStore.recentsList

View File

@ -16,8 +16,9 @@ ColumnLayout {
property string nftName
property string nftUrl
property string tokenId
property string contractAddress
property string tokenAddress
property bool strikethrough: false
property bool areTestNetworksEnabled: false
spacing: Style.current.padding
@ -100,8 +101,11 @@ ColumnLayout {
icon.name: "external"
type: StatusRoundButton.Type.Quinary
radius: 8
visible: nftPreviewSensor.hovered && !!root.tokenId && !!root.contractAddress
onClicked: Global.openLink("https://etherscan.io/nft/%1/%2".arg(root.contractAddress).arg(root.tokenId))
visible: nftPreviewSensor.hovered && !!root.tokenId && !!root.tokenAddress
onClicked: {
const link = areTestNetworksEnabled ? Constants.networkExplorerLinks.goerliEtherscan : Constants.networkExplorerLinks.etherscan
Global.openLink("%1/nft/%2/%3".arg(link).arg(root.tokenAddress).arg(root.tokenId))
}
}
}
}

View File

@ -34,7 +34,7 @@ StatusMenu {
property var collectiblesList: []
property var collectiblesFilter: []
readonly property bool allCollectiblesChecked: tokensMenu.allCollectiblesChecked
signal updateCollectiblesFilter(double id)
signal updateCollectiblesFilter(string uid)
// Recents filter
property var recentsList
@ -52,6 +52,11 @@ StatusMenu {
implicitWidth: 176
function resetView() {
counterPartyMenu.resetView()
tokensMenu.resetView()
}
// Filter By Period
ActivityFilterMenuItem {
text: qsTr("Period")
@ -92,7 +97,7 @@ StatusMenu {
collectiblesList: root.collectiblesList
collectiblesFilter: root.collectiblesFilter
onTokenToggled: updateTokensFilter(tokenSymbol)
onCollectibleToggled: updateCollectiblesFilter(id)
onCollectibleToggled: updateCollectiblesFilter(uid)
closePolicy: root.closePolicy
}
ActivityCounterpartyFilterSubMenu {

View File

@ -40,6 +40,10 @@ StatusMenu {
Component.onCompleted: root.updateRecipientsModel()
function resetView() {
searchBox.reset()
}
contentItem: ColumnLayout {
spacing: 12

View File

@ -26,15 +26,20 @@ StatusMenu {
signal back()
signal tokenToggled(string tokenSymbol)
signal collectibleToggled(double id)
signal collectibleToggled(string uid)
property var searchTokenSymbolByAddressFn: function (address) { return "" } // TODO
implicitWidth: 289
function resetView() {
tokensSearchBox.reset()
collectiblesSearchBox.reset()
}
QtObject {
id: d
property bool isFetching: root.collectiblesList.isFetching
readonly property bool isFetching: root.collectiblesList.isFetching
}
contentItem: ColumnLayout {
@ -99,12 +104,14 @@ StatusMenu {
Layout.fillHeight: true
spacing: 0
model: SortFilterProxyModel {
id: tokenProxyModel
sourceModel: root.tokensList
filters: ExpressionFilter {
readonly property string tokenSearchValue: tokensSearchBox.text.toUpperCase()
readonly property string tokenSymbolByAddress: root.searchTokenSymbolByAddressFn(tokensSearchBox.text)
enabled: root.tokensList.count > 0
expression: {
const tokenSymbolByAddress = root.searchTokenSymbolByAddressFn(tokensSearchBox.text)
return symbol.startsWith(tokensSearchBox.text.toUpperCase()) || name.toUpperCase().startsWith(tokensSearchBox.text.toUpperCase()) || (tokenSymbolByAddress!=="" && symbol.startsWith(tokenSymbolByAddress))
return symbol.startsWith(tokenSearchValue) || name.toUpperCase().startsWith(tokenSearchValue) || (tokenSymbolByAddress!=="" && symbol.startsWith(tokenSymbolByAddress))
}
}
}
@ -155,28 +162,30 @@ StatusMenu {
spacing: 0
reuseItems: true
model: SortFilterProxyModel {
id: collectibleProxyModel
sourceModel: root.collectiblesList
filters: ExpressionFilter {
enabled: root.collectiblesList.count > 0 && !!collectiblesSearchBox.text
readonly property string searchText: collectiblesSearchBox.text.toUpperCase()
expression: {
const searchText = collectiblesSearchBox.text.toUpperCase()
return name.toUpperCase().startsWith(searchText)
return String(name).toUpperCase().startsWith(searchText)
}
}
}
delegate: ActivityTypeCheckBox {
required property var model
width: ListView.view.width
height: 44
title: model.name
assetSettings.name: model.imageUrl
title: model.name ?? ""
assetSettings.name: model.imageUrl ?? ""
assetSettings.isImage: true
assetSettings.bgWidth: 32
assetSettings.bgHeight: 32
assetSettings.bgRadius: assetSettings.bgHeight/2
buttonGroup: collectibleButtonGroup
allChecked: root.allCollectiblesChecked
checked: root.allCollectiblesChecked || root.collectiblesFilter.includes(model.id)
onActionTriggered: root.collectibleToggled(model.id)
checked: !loading && (root.allCollectiblesChecked || root.collectiblesFilter.includes(model.uid))
onActionTriggered: root.collectibleToggled(model.uid)
loading: d.isFetching
}
}

View File

@ -191,15 +191,13 @@ QtObject {
// To-do: Get list of collectibles with activity from backend
property var collectiblesList: walletSection.collectiblesController.model
property var collectiblesFilter: []
function toggleCollectibles(id) {
function toggleCollectibles(uid) {
// update filters
collectiblesFilter = d.toggleFilterState(collectiblesFilter, id, collectiblesList.count)
// TODO go side filtering is pending
// activityController.setFilterCollectibles(JSON.stringify(collectiblesFilter))
// activityController.updateFilter()
collectiblesFilter = d.toggleFilterState(collectiblesFilter, uid, collectiblesList.count)
activityController.setFilterCollectibles(JSON.stringify(collectiblesFilter))
activityController.updateFilter()
}
property var recentsList: activityController.recipientsModel
property bool loadingRecipients: activityController.status.loadingRecipients
property var recentsFilters: []
@ -251,7 +249,7 @@ QtObject {
activityController.setFilterStatus(JSON.stringify(statusFilters))
activityController.setFilterAssets(JSON.stringify(tokensFilter), false)
activityController.setFilterToAddresses(JSON.stringify(recentsFilters.concat(savedAddressFilters)))
// TODO call update filter for collectibles
activityController.setFilterCollectibles(JSON.stringify(collectiblesFilter))
activityController.updateFilter()
}
@ -266,7 +264,6 @@ QtObject {
collectiblesFilter = []
recentsFilters = []
savedAddressFilters = []
// TODO reset filter for collectibles
applyAllFilters()
}

View File

@ -30,6 +30,7 @@ Item {
function resetView() {
stack.currentIndex = 0
root.currentTabIndex = 0
historyView.resetView()
}
function resetStack() {
@ -141,6 +142,7 @@ Item {
}
}
HistoryView {
id: historyView
overview: RootStore.overview
showAllAccounts: root.showAllAccounts
onLaunchTransactionDetail: function (entry, entryIndex) {

View File

@ -40,7 +40,7 @@ Item {
property var details: null
readonly property bool isDetailsValid: details !== undefined && !!details
readonly property bool isIncoming: transactionType === Constants.TransactionType.Received
readonly property bool isIncoming: transactionType === Constants.TransactionType.Received || transactionType === Constants.TransactionType.ContractDeployment
readonly property string networkShortName: root.isTransactionValid ? RootStore.getNetworkShortName(transaction.chainId) : ""
readonly property string networkIcon: isTransactionValid ? RootStore.getNetworkIcon(transaction.chainId) : "network/Network=Custom"
readonly property int blockNumber: isDetailsValid ? details.blockNumber : 0
@ -188,7 +188,8 @@ Item {
nftUrl: root.isTransactionValid && !!transaction.nftImageUrl ? transaction.nftImageUrl : ""
strikethrough: d.transactionType === Constants.TransactionType.Destroy
tokenId: root.isTransactionValid ? transaction.tokenID : ""
contractAddress: d.isDetailsValid ? d.details.contract : ""
tokenAddress: root.isTransactionValid ? transaction.tokenAddress : ""
areTestNetworksEnabled: WalletStores.RootStore.areTestNetworksEnabled
}
Column {
@ -561,6 +562,7 @@ Item {
RowLayout {
width: parent.width
visible: amountSentTile.visible || amountReceiveTile.visible || feesTile.visible || totalTile.visible
StatusBaseText {
Layout.alignment: Qt.AlignLeft
font.pixelSize: 15
@ -580,6 +582,7 @@ Item {
DetailsPanel {
TransactionDataTile {
id: amountSentTile
width: parent.width
title: qsTr("Amount sent")
subTitle: transactionHeader.isMultiTransaction ? d.outCryptoValueFormatted : d.cryptoValueFormatted
@ -598,6 +601,7 @@ Item {
}
}
TransactionDataTile {
id: amountReceiveTile
width: parent.width
title: transactionHeader.transactionStatus === Constants.TransactionStatus.Pending ? qsTr("Amount to receive") : qsTr("Amount received")
subTitle: {
@ -625,6 +629,7 @@ Item {
visible: !!subTitle
}
TransactionDataTile {
id: feesTile
width: parent.width
title: d.symbol ? qsTr("Fees") : qsTr("Estimated max fee")
subTitle: {
@ -659,6 +664,7 @@ Item {
visible: !!subTitle
}
TransactionDataTile {
id: totalTile
width: parent.width
readonly property bool fieldIsHidden: (transactionHeader.isNFT && d.isIncoming) || !d.symbol
readonly property bool showMaxFee: d.transactionType === Constants.TransactionType.ContractDeployment && transactionHeader.transactionStatus === Constants.TransactionStatus.Pending

View File

@ -504,8 +504,8 @@ StatusListItem {
return qsTr("Via %1 on %2").arg(name).arg(networkName)
case Constants.TransactionType.Mint:
if (allAccounts)
return qsTr("%1 via %2 in %4").arg(transactionValue).arg(networkName).arg(toAddress)
return qsTr("%1 via %2").arg(transactionValue).arg(networkName).arg(networkName)
return qsTr("%1 via %2 in %3").arg(transactionValue).arg(networkName).arg(toAddress)
return qsTr("%1 via %2").arg(transactionValue).arg(networkName)
default:
if (allAccounts)
return qsTr("%1 from %2 to %3 via %4").arg(transactionValue).arg(fromAddress).arg(toAddress).arg(networkName)

View File

@ -31,6 +31,12 @@ ColumnLayout {
signal launchTransactionDetail(var transaction, int entryIndex)
function resetView() {
if (!!filterPanelLoader.item) {
filterPanelLoader.item.resetView()
}
}
onVisibleChanged: {
if (!visible)
return