fix(wallet): Added adaptor to properly refresh send modal total balances (#15409)

This commit is contained in:
Cuteivist 2024-07-02 14:22:20 +02:00 committed by GitHub
parent c1cc74750f
commit e199e7f9da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 480 additions and 237 deletions

View File

@ -0,0 +1,240 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Models 0.1
import Storybook 1.0
import utils 1.0
import shared.popups.send.models 1.0
Item {
id: root
ListModel {
id: listModel
readonly property var data: [
{
tokensKey: "key_ETH",
name: "Ether",
symbol: "ETH",
addressPerChain: [
{ chainId: 1, address: "0x0000000000000000000000000000000000000000"},
{ chainId: 5, address: "0x0000000000000000000000000000000000000000"},
{ chainId: 10, address: "0x0000000000000000000000000000000000000000"},
{ chainId: 420, address: "0x0000000000000000000000000000000000000000"},
{ chainId: 42161, address: "0x0000000000000000000000000000000000000000"},
{ chainId: 421613, address: "0x0000000000000000000000000000000000000000"}
],
balances: [
{
chainId: "chain_id_1",
balance: "186316672770338050",
account: "account_1",
},
{
chainId: "chain_id_1",
balance: "386318672772348050",
account: "account_2",
},
{
chainId: "chain_id_2",
balance: "186311232772348990",
account: "account_1",
},
{
chainId: "chain_id_2",
balance: "986317232772348990",
account: "account_1",
}
],
decimals: 18,
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl(""),
marketDetails: {
changePct24hour: -2.1232,
currencyPrice: {
amount: 3423.23898
}
},
detailsLoading: false,
image: Qt.resolvedUrl("")
},
{
tokensKey: "key_SNT",
name: "Status",
symbol: "SNT",
addressPerChain: [
{ chainId: 1, address: "0x0000000000000000000000000000000000000000"},
{ chainId: 5, address: "0x0000000000000000000000000000000000000000"},
{ chainId: 10, address: "0x0000000000000000000000000000000000000000"},
{ chainId: 420, address: "0x0000000000000000000000000000000000000000"},
{ chainId: 42161, address: "0x0000000000000000000000000000000000000000"},
{ chainId: 421613, address: "0x0000000000000000000000000000000000000000"}
],
balances: [
{
chainId: "chain_id_1",
balance: "386316672770338850",
account: "account_1",
},
{
chainId: "chain_id_1",
balance: "377778672772348050",
account: "account_2",
},
{
chainId: "chain_id_2",
balance: "146311232772348990",
account: "account_1",
},
{
chainId: "chain_id_3",
balance: "86317232772348990",
account: "account_1",
}
],
decimals: 18,
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl(""),
marketDetails: {
changePct24hour: 9.232,
currencyPrice: {
amount: 33.23898
}
},
detailsLoading: false,
image: Qt.resolvedUrl("")
},
{
tokensKey: "key_MYASST",
name: "Community Asset",
symbol: "MYASST",
balances: [
{
chainId: "chain_id_1",
balance: "23234",
account: "account_1",
},
{
chainId: "chain_id_1",
balance: "63234",
account: "account_2",
}
],
decimals: 3,
communityId: "0x033f36ccb",
communityName: "My Community",
communityImage: Constants.tokenIcon("DAI", false),
marketDetails: {
changePct24hour: 0,
currencyPrice: {
amount: 0
}
},
detailsLoading: false,
image: Constants.tokenIcon("ZRX", false)
}
]
Component.onCompleted: {
append(data)
const accounts = new Set()
data.forEach(e => e.balances.forEach(
e => { accounts.add(e.account) }))
accountsSelector.model = [...accounts.values()]
}
}
ManageTokensController {
id: manageTokensController
sourceModel: listModel
serializeAsCollectibles: false
onRequestLoadSettings: {
loadingStarted()
const jsonData = [
{
"key": "ETH",
"position": 1,
"visible": true
},
{
"key": "SNT",
"position": 2,
"visible": true
},
{
"key": "MYASST",
"position": 5,
"visible": true
}
]
loadingFinished(JSON.stringify(jsonData))
}
}
SendModalAssetsAdaptor {
id: adaptor
controller: manageTokensController
account: accountsSelector.selection[0]
tokensModel: listModel
}
ColumnLayout {
anchors.fill: parent
Label { text: "ACCOUNTS:" }
CheckBoxFlowSelector {
id: accountsSelector
Layout.fillWidth: true
initialSelection: true
exclusive: true
}
RowLayout {
GenericListView {
label: "Input model"
model: listModel
Layout.fillWidth: true
Layout.fillHeight: true
skipEmptyRoles: true
}
GenericListView {
label: "Adapter's output model"
model: adaptor.model
Layout.fillWidth: true
Layout.fillHeight: true
roles:
["key", "error", "currentBalance", "currentCurrencyBalance", "currentBalanceText",
"icon", "visible", "marketDetailsAvailable", "marketDetailsLoading",
"marketPrice", "marketChangePct24hour", "isCommunityAsset", "balancesModel"]
skipEmptyRoles: true
}
}
}
}
// category: Adaptors

View File

@ -13,6 +13,12 @@ Flow {
property var selection: []
property bool initialSelection
property bool exclusive: false
ButtonGroup {
id: checkboxGroup
exclusive: root.exclusive
}
Repeater {
id: repeater
@ -33,6 +39,7 @@ Flow {
text: modelData
checked: root.initialSelection
onToggled: repeater.update()
ButtonGroup.group: checkboxGroup
}
onItemAdded: update()

View File

@ -88,26 +88,6 @@ QtObject {
return SQUtils.ModelUtils.get(nestedCollectiblesModel, idx)
}
function getHolding(holdingId, holdingType) {
if (holdingType === Constants.TokenType.ERC20) {
return getAsset(processedAssetsModel, holdingId)
} else if (holdingType === Constants.TokenType.ERC721) {
return getCollectible(holdingId)
} else {
return {}
}
}
function getSelectorHolding(holdingId, holdingType) {
if (holdingType === Constants.TokenType.ERC20) {
return getAsset(processedAssetsModel, holdingId)
} else if (holdingType === Constants.TokenType.ERC721) {
return getSelectorCollectible(holdingId)
} else {
return {}
}
}
function assetToSelectorAsset(asset) {
return asset
}
@ -254,84 +234,4 @@ QtObject {
property bool showCommunityAssetsInSend: true
property bool balanceThresholdEnabled: true
property real balanceThresholdAmount
// Property set from TokenLIstView and HoldingSelector to search token by name, symbol or contract address
property string assetSearchString
// Model prepared to provide filtered and sorted assets as per the advanced Settings in token management
property var processedAssetsModel: SortFilterProxyModel {
sourceModel: walletAssetStore.groupedAccountAssetsModel
proxyRoles: [
FastExpressionRole {
name: "isCommunityAsset"
expression: !!model.communityId
expectedRoles: ["communityId"]
},
FastExpressionRole {
name: "currentBalance"
expression: __getTotalBalance(model.balances, model.decimals)
expectedRoles: ["balances", "decimals", "symbol"]
},
FastExpressionRole {
name: "currentCurrencyBalance"
expression: {
if (!!model.marketDetails) {
return model.currentBalance * model.marketDetails.currencyPrice.amount
}
return 0
}
expectedRoles: ["marketDetails", "currentBalance"]
}
]
filters: [
FastExpressionFilter {
function search(symbol, name, addressPerChain, searchString) {
return (
symbol.startsWith(searchString.toUpperCase()) ||
name.toUpperCase().startsWith(searchString.toUpperCase()) || __searchAddressInList(addressPerChain, searchString)
)
}
expression: search(model.symbol, model.name, model.addressPerChain, root.assetSearchString)
expectedRoles: ["symbol", "name", "addressPerChain"]
},
ValueFilter {
roleName: "isCommunityAsset"
value: false
enabled: !showCommunityAssetsInSend
},
FastExpressionFilter {
expression: {
return model.currentCurrencyBalance > balanceThresholdAmount
}
expectedRoles: ["currentCurrencyBalance"]
enabled: balanceThresholdEnabled
}
]
sorters: RoleSorter {
roleName: "isCommunityAsset"
}
}
/* Internal function to search token address */
function __searchAddressInList(addressPerChain, searchString) {
let addressFound = false
let tokenAddresses = SQUtils.ModelUtils.modelToFlatArray(addressPerChain, "address")
for (let i =0; i< tokenAddresses.length; i++){
if(tokenAddresses[i].toUpperCase().startsWith(searchString.toUpperCase())) {
addressFound = true
break;
}
}
return addressFound
}
/* Internal function to calculate total balance */
function __getTotalBalance(balances, decimals) {
let totalBalance = 0
for(let i=0; i<balances.count; i++) {
let balancePerAddressPerChain = SQUtils.ModelUtils.get(balances, i)
totalBalance+=SQUtils.AmountsArithmetic.toNumber(balancePerAddressPerChain.balance, decimals)
}
return totalBalance
}
}

View File

@ -25,6 +25,7 @@ import AppLayouts.Wallet.controls 1.0
import "./panels"
import "./controls"
import "./views"
import "./models"
StatusDialog {
id: popup
@ -102,8 +103,18 @@ StatusDialog {
property var hoveredHoldingType: Constants.TokenType.Unknown
readonly property bool isHoveredHoldingValidAsset: !!hoveredHolding && hoveredHoldingType === Constants.TokenType.ERC20
function getHolding(holdingId, holdingType) {
if (holdingType === Constants.TokenType.ERC20) {
return store.getAsset(assetsAdaptor.model, holdingId)
} else if (holdingType === Constants.TokenType.ERC721 || holdingType === Constants.TokenType.ERC1155) {
return store.getCollectible(holdingId)
} else {
return {}
}
}
function setSelectedHoldingId(holdingId, holdingType) {
let holding = store.getHolding(holdingId, holdingType)
let holding = getHolding(holdingId, holdingType)
setSelectedHolding(holding, holdingType)
}
@ -115,7 +126,7 @@ StatusDialog {
}
function setHoveredHoldingId(holdingId, holdingType) {
let holding = store.getHolding(holdingId, holdingType)
let holding = getHolding(holdingId, holdingType)
setHoveredHolding(holding, holdingType)
}
@ -148,6 +159,19 @@ StatusDialog {
}
}
SendModalAssetsAdaptor {
id: assetsAdaptor
controller: popup.store.walletAssetStore.assetsController
showCommunityAssets: popup.store.tokensStore.showCommunityAssetsInSend
tokensModel: popup.store.walletAssetStore.groupedAccountAssetsModel
account: popup.store.selectedSenderAccount.address
marketValueThreshold:
popup.store.tokensStore.displayAssetsBelowBalance
? popup.store.tokensStore.getDisplayAssetsBelowBalanceThresholdDisplayAmount()
: 0
}
bottomPadding: 16
padding: 0
background: StatusDialogBackground {
@ -273,7 +297,8 @@ StatusDialog {
id: holdingSelector
Layout.fillWidth: true
Layout.fillHeight: true
assetsModel: popup.store.processedAssetsModel
// assetsModel: popup.store.processedAssetsModel
assetsModel: assetsAdaptor.model
collectiblesModel: popup.preSelectedAccount ? popup.nestedCollectiblesModel : null
networksModel: popup.store.flatNetworksModel
visible: (!!d.selectedHolding && d.selectedHoldingType !== Constants.TokenType.Unknown) ||
@ -281,7 +306,7 @@ StatusDialog {
onItemSelected: {
d.setSelectedHoldingId(holdingId, holdingType)
}
onSearchTextChanged: popup.store.assetSearchString = searchText
onSearchTextChanged: assetsAdaptor.assetSearchString = assetSearchString
formatCurrentCurrencyAmount: function(balance){
return popup.store.currencyStore.formatCurrencyAmount(balance, popup.store.currencyStore.currentCurrency)
}
@ -391,7 +416,7 @@ StatusDialog {
Layout.bottomMargin: Style.current.xlPadding
visible: !d.selectedHolding
assets: popup.store.processedAssetsModel
assets: assetsAdaptor.model
collectibles: popup.preSelectedAccount ? popup.nestedCollectiblesModel : null
networksModel: popup.store.flatNetworksModel
onlyAssets: holdingSelector.onlyAssets
@ -405,7 +430,7 @@ StatusDialog {
d.setHoveredHoldingId("", Constants.TokenType.Unknown)
}
}
onAssetSearchStringChanged: store.assetSearchString = assetSearchString
onAssetSearchStringChanged: assetsAdaptor.assetSearchString = assetSearchString
formatCurrentCurrencyAmount: function(balance){
return popup.store.currencyStore.formatCurrencyAmount(balance, popup.store.currencyStore.currentCurrency)
}

View File

@ -0,0 +1,199 @@
import QtQml 2.15
import StatusQ 0.1
import StatusQ.Models 0.1
import StatusQ.Core.Utils 0.1
import utils 1.0
import SortFilterProxyModel 0.2
QObject {
id: root
// Controller providing information about visibility and order defined
// by a user (token management)
required property ManageTokensController controller
property bool showCommunityAssets: false
property string assetSearchString: ""
/**
Expected model structure:
Tokens related part:
tokensKey [string] - unique identifier of a token, e.g "0x3234235"
symbol [string] - token's symbol e.g. "ETH" or "SNT"
name [string] - token's name e.g. "Ether" or "Dai"
image [url] - token's icon for custom tokens
decimals [int] - number of decimal places, e.g. 18 for ETH
balances [model] - submodel of balances per chain/account
chainId [string] - unique identifier of a chain
account [string] - unique identifier of an account
balance [string] - balance in basic unit as big integer string
marketDetails [object] - object holding market details
changePct24hour [double] - percentage change of fiat price in last day
currencyPrice [object] - object holding fiat price details
amount [double] - fiat prace of 1 logical unit of cryptocurrency
detailsLoading [bool] - indicatator if market details are ready to use
addressPerChain [model] - submodel of addresses per chain
chainId [string] - unique identifier of a chain
address [string] - address of a token contract
Community related part (relevant for community minted assets, empty otherwise):
communityId [string] - unique identifier of a community, e.g. "0x6734235"
**/
property var tokensModel
// function formatting tokens balance expressed in a commonly used units,
// e.g. 1.2 for 1.2 ETH, according to rules specific for given symbol
property var formatBalance:
(balance, symbol) => `${balance.toLocaleString(Qt.locale())} ${symbol}`
// account used for balance calculation
property string account: ""
// threshold below which the token is omitted from the output model
property double marketValueThreshold
/**
Model structure:
All roles from the source model are passed directly to the output model,
additionally:
key [string] - renamed from tokensKey
icon [url] - from image or fetched by symbol for well-known tokens
currentbalance [double] - tokens balance is the commonly used unit, e.g. 1.2 for 1.2 ETH,
computed from balances according to provided criteria
currentBalanceText [string] - formatted and localized balance
currentCurrencyBalance [double] - tokens fiat balance computed from balance and market price
marketDetailsAvailable [bool] - specifies if market datails are available for given token
marketDetailsLoading [bool] - specifies if market datails are available for given token
marketPrice [double] - specifies market price in currently used currency
marketChangePct24hour [double] - percentage price change in last 24 hours, e.g. 0.5 for 0.5% of price change
balancesModel [model] - filtered balances model by selected account
**/
readonly property alias model: sfpm
ObjectProxyModel {
id: proxyModel
sourceModel: root.tokensModel ?? null
delegate: QObject {
readonly property var rootModel: model
readonly property bool isCommunityAsset: !!model.communityId
readonly property var marketDetails: model.marketDetails
// Read-only roles exposed to the model:
readonly property string key: model.tokensKey
readonly property double currentBalance: AmountsArithmetic.toNumber(totalBalanceAggregator.value, model.decimals)
readonly property double currentCurrencyBalance: currentBalance * marketPrice
readonly property string currentBalanceText: root.formatBalance(currentBalance, model.symbol)
readonly property bool marketDetailsAvailable: !isCommunityAsset
readonly property bool marketDetailsLoading: model.detailsLoading
readonly property real marketPrice: marketDetails.currencyPrice.amount ?? 0
readonly property real marketChangePct24hour: marketDetails.changePct24hour ?? 0
readonly property bool visible: {
root.controller.revision
if (!root.controller.filterAcceptsSymbol(model.symbol))
return false
if (isCommunityAsset) {
return root.showCommunityAssets
}
return currentCurrencyBalance >= root.marketValueThreshold
}
readonly property url icon: !!model.image ? model.image : Constants.tokenIcon(model.symbol, false)
readonly property var balancesModel: filteredBalances
SortFilterProxyModel {
id: filteredBalances
sourceModel: rootModel.balances
filters: [
FastExpressionFilter {
expression: root.account === model.account
expectedRoles: ["account"]
}
]
}
FunctionAggregator {
id: totalBalanceAggregator
model: filteredBalances
initialValue: "0"
roleName: "balance"
aggregateFunction: (aggr, value) => AmountsArithmetic.sum(
AmountsArithmetic.fromString(aggr),
AmountsArithmetic.fromString(value)).toString()
}
}
expectedRoles:
["tokensKey", "symbol", "image", "balances", "decimals",
"detailsLoading", "marketDetails", "communityId", "addressPerChain"]
exposedRoles:
["key", "error", "currentBalance", "currentCurrencyBalance", "currentBalanceText",
"icon", "visible", "marketDetailsAvailable", "marketDetailsLoading",
"marketPrice", "marketChangePct24hour", "isCommunityAsset", "balancesModel"]
/* Internal function to search token address */
function __searchAddressInList(addressPerChain, searchString) {
const uppercaseSearchString = searchString.toUpperCase()
let addressFound = false
let tokenAddresses = ModelUtils.modelToFlatArray(addressPerChain, "address")
for (let i =0; i< tokenAddresses.length; i++){
if(tokenAddresses[i].toUpperCase().startsWith(uppercaseSearchString)) {
addressFound = true
break;
}
}
return addressFound
}
}
SortFilterProxyModel {
id: sfpm
sourceModel: proxyModel
objectName: "SendModalAssetsAdaptorModel"
filters: [
FastExpressionFilter {
function search(symbol, name, addressPerChain, searchString) {
const uppercaseSearchString = searchString.toUpperCase()
return (
symbol.toUpperCase().startsWith(uppercaseSearchString) ||
name.toUpperCase().startsWith(uppercaseSearchString) || proxyModel.__searchAddressInList(addressPerChain, searchString)
)
}
expression: search(symbol, name, addressPerChain, root.assetSearchString)
expectedRoles: ["symbol", "name", "addressPerChain"]
},
ValueFilter {
roleName: "visible"
value: true
}
]
sorters: RoleSorter {
roleName: "isCommunityAsset"
}
}
}

View File

@ -0,0 +1 @@
SendModalAssetsAdaptor 1.0 SendModalAssetsAdaptor.qml

View File

@ -211,7 +211,7 @@ Item {
property var totalBalance: model.totalBalance
property var marketDetails: model.marketDetails
property var decimals: model.decimals
property var balances: model.balances
property var balances: model.balancesModel
// collectible
property var uid: model.uid
property var iconUrl: model.iconUrl

View File

@ -221,7 +221,7 @@ Item {
width: tokenList.width
balancesModel: LeftJoinModel {
leftModel: !!model & !!model.balances ? model.balances : null
leftModel: !!model & !!model.balancesModel ? model.balancesModel : null
rightModel: root.networksModel
joinRole: "chainId"
}

View File

@ -110,26 +110,6 @@ QtObject {
return ModelUtils.get(nestedCollectiblesModel, idx)
}
function getHolding(holdingId, holdingType) {
if (holdingType === Constants.TokenType.ERC20) {
return getAsset(processedAssetsModel, holdingId)
} else if (holdingType === Constants.TokenType.ERC721 || holdingType === Constants.TokenType.ERC1155) {
return getCollectible(holdingId)
} else {
return {}
}
}
function getSelectorHolding(holdingId, holdingType) {
if (holdingType === Constants.TokenType.ERC20) {
return getAsset(processedAssetsModel, holdingId)
} else if (holdingType === Constants.TokenType.ERC721 || holdingType === Constants.TokenType.ERC1155) {
return getSelectorCollectible(holdingId)
} else {
return {}
}
}
function assetToSelectorAsset(asset) {
return asset
}
@ -223,7 +203,6 @@ QtObject {
}
function resetStoredProperties() {
assetSearchString = ""
walletSectionSendInst.resetStoredProperties()
nestedCollectiblesModel.currentCollectionUid = ""
}
@ -242,112 +221,4 @@ QtObject {
function formatCurrencyAmountFromBigInt(balance, symbol, decimals, options = null) {
return currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, options)
}
// Property set from TokenLIstView and HoldingSelector to search token by name, symbol or contract address
property string assetSearchString
// Internal model filtering balances by the account selected on the SendModalPage
property SubmodelProxyModel __assetsWithFilteredBalances: SubmodelProxyModel {
sourceModel: walletAssetStore.groupedAccountAssetsModel
submodelRoleName: "balances"
delegateModel: SortFilterProxyModel {
sourceModel: submodel
filters: [
ValueFilter {
roleName: "account"
value: root.selectedSenderAccount.address
}
]
}
}
readonly property Connections tokensStoreConnections: Connections {
target: tokensStore
function onDisplayAssetsBelowBalanceThresholdChanged() {
processedAssetsModel.displayAssetsBelowBalanceThresholdAmount = tokensStore.getDisplayAssetsBelowBalanceThresholdDisplayAmount()
}
}
// Model prepared to provide filtered and sorted assets as per the advanced Settings in token management
property var processedAssetsModel: SortFilterProxyModel {
property real displayAssetsBelowBalanceThresholdAmount: tokensStore.getDisplayAssetsBelowBalanceThresholdDisplayAmount()
sourceModel: __assetsWithFilteredBalances
proxyRoles: [
FastExpressionRole {
name: "isCommunityAsset"
expression: !!model.communityId
expectedRoles: ["communityId"]
},
FastExpressionRole {
name: "currentBalance"
expression: __getTotalBalance(model.balances, model.decimals)
expectedRoles: ["balances", "decimals"]
},
FastExpressionRole {
name: "currentCurrencyBalance"
expression: {
if (!!model.marketDetails) {
return model.currentBalance * model.marketDetails.currencyPrice.amount
}
return 0
}
expectedRoles: ["marketDetails", "currentBalance"]
}
]
filters: [
FastExpressionFilter {
function search(symbol, name, addressPerChain, searchString) {
return (
symbol.toUpperCase().startsWith(searchString.toUpperCase()) ||
name.toUpperCase().startsWith(searchString.toUpperCase()) || __searchAddressInList(addressPerChain, searchString)
)
}
expression: search(symbol, name, addressPerChain, root.assetSearchString)
expectedRoles: ["symbol", "name", "addressPerChain"]
},
ValueFilter {
roleName: "isCommunityAsset"
value: false
enabled: !tokensStore.showCommunityAssetsInSend
},
FastExpressionFilter {
expression: {
root.walletAssetStore.assetsController.revision
if (!root.walletAssetStore.assetsController.filterAcceptsSymbol(model.symbol)) // explicitely hidden
return false
if (tokensStore.displayAssetsBelowBalance)
return model.currentCurrencyBalance > processedAssetsModel.displayAssetsBelowBalanceThresholdAmount
return true
}
expectedRoles: ["symbol", "currentCurrencyBalance"]
}
]
sorters: RoleSorter {
roleName: "isCommunityAsset"
}
}
/* Internal function to search token address */
function __searchAddressInList(addressPerChain, searchString) {
let addressFound = false
let tokenAddresses = ModelUtils.modelToFlatArray(addressPerChain, "address")
for (let i =0; i< tokenAddresses.length; i++){
if(tokenAddresses[i].toUpperCase().startsWith(searchString.toUpperCase())) {
addressFound = true
break;
}
}
return addressFound
}
/* Internal function to calculate total balance */
function __getTotalBalance(balances, decimals) {
let totalBalance = 0
for(let i=0; i<balances.count; i++) {
let balancePerAddressPerChain = ModelUtils.get(balances, i)
totalBalance+=AmountsArithmetic.toNumber(balancePerAddressPerChain.balance, decimals)
}
return totalBalance
}
}