From 0d4d1b0ba7f863012ec762e012191cf0e6f27017 Mon Sep 17 00:00:00 2001 From: Khushboo Mehta Date: Thu, 28 Nov 2024 12:42:31 +0100 Subject: [PATCH] feat(@desktop/wallet): Move the Account Selector logic to show selected token balance on a sepcific network to a dedicated WalletAccountsSelectorAdaptor fixes #16705 --- storybook/pages/AccountSelectorPage.qml | 215 +++++++++++++++--- storybook/pages/SimpleSendModalPage.qml | 43 +++- .../WalletAccountsSelectorAdaptorPage.qml | 187 +++++++++++++++ storybook/qmlTests/tests/tst_SwapModal.qml | 88 ++++--- .../tst_WalletAccountsSelectorAdaptor.qml | 208 +++++++++++++++++ .../WalletAccountsSelectorAdaptor.qml | 152 +++++++++++++ ui/app/AppLayouts/Wallet/adaptors/qmldir | 1 + .../popups/simpleSend/SimpleSendModal.qml | 3 - .../Wallet/popups/swap/SwapModal.qml | 55 +++-- .../Wallet/popups/swap/SwapModalAdaptor.qml | 106 --------- ui/app/mainui/AppMain.qml | 15 +- ui/app/mainui/SendModalHandler.qml | 35 ++- 12 files changed, 906 insertions(+), 202 deletions(-) create mode 100644 storybook/pages/WalletAccountsSelectorAdaptorPage.qml create mode 100644 storybook/qmlTests/tests/tst_WalletAccountsSelectorAdaptor.qml create mode 100644 ui/app/AppLayouts/Wallet/adaptors/WalletAccountsSelectorAdaptor.qml diff --git a/storybook/pages/AccountSelectorPage.qml b/storybook/pages/AccountSelectorPage.qml index bd840b5613..b1688d7fd3 100644 --- a/storybook/pages/AccountSelectorPage.qml +++ b/storybook/pages/AccountSelectorPage.qml @@ -7,48 +7,205 @@ import Models 1.0 import SortFilterProxyModel 0.2 import shared.controls 1.0 +import shared.stores 1.0 -Item { +import AppLayouts.Wallet.stores 1.0 +import AppLayouts.Wallet.adaptors 1.0 + +import utils 1.0 + +SplitView { id: root - ColumnLayout { - spacing: 16 - anchors.centerIn: parent - implicitWidth: 150 + orientation: Qt.Vertical + QtObject { + id: d - WalletAccountsModel { - id: accountsModel + readonly property var flatNetworks: NetworksModel.flatNetworks + readonly property var assetsStore: WalletAssetsStore { + id: thisWalletAssetStore + walletTokensStore: TokensStore { + plainTokensBySymbolModel: TokensBySymbolModel {} + } + readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} + assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel } - Label { - text: "Default style" - font.bold: true - Layout.fillWidth: true + readonly property var currencyStore: CurrenciesStore{} + readonly property var nonWatchWalletAcounts: SortFilterProxyModel { + sourceModel: walletAccountsModel + filters: ValueFilter { roleName: "canSend"; value: true } } - AccountSelector { - id: accountSelector - Layout.fillWidth: true - model: WalletAccountsModel {} - onCurrentAccountAddressChanged: { - accountSelector2.selectedAddress = currentAccountAddress + + readonly property var filteredFlatNetworksModel: SortFilterProxyModel { + sourceModel: d.flatNetworks + filters: ValueFilter { roleName: "isTest"; value: true } + } + } + + ListModel { + id: walletAccountsModel + readonly property var data: [ + { + name: "helloworld", + address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", + emoji: "😋", + colorId: Constants.walletAccountColors.primary, + walletType: "", + canSend: true, + position: 0, + currencyBalance: ({amount: 1.25, + symbol: "USD", + displayDecimals: 2, + stripTrailingZeroes: false}), + migratedToKeycard: true + }, + { + name: "Hot wallet (generated)", + emoji: "🚗", + colorId: Constants.walletAccountColors.army, + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", + walletType: Constants.generatedWalletType, + canSend: true, + position: 3, + currencyBalance: ({amount: 10, + symbol: "USD", + displayDecimals: 2, + stripTrailingZeroes: false}), + migratedToKeycard: false + }, + { + name: "Family (seed)", + emoji: "🎨", + colorId: Constants.walletAccountColors.magenta, + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8882", + walletType: Constants.seedWalletType, + canSend: true, + position: 1, + currencyBalance: ({amount: 110.05, + symbol: "USD", + displayDecimals: 2, + stripTrailingZeroes: false}), + migratedToKeycard: false + }, + { + name: "Tag Heuer (watch)", + emoji: "⌚", + colorId: Constants.walletAccountColors.copper, + color: "#CB6256", + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8883", + walletType: Constants.watchWalletType, + canSend: false, + position: 2, + currencyBalance: ({amount: 3, + symbol: "USD", + displayDecimals: 2, + stripTrailingZeroes: false}), + migratedToKeycard: false + }, + { + name: "Fab (key)", + emoji: "🔑", + colorId: Constants.walletAccountColors.camel, + color: "#C78F67", + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8884", + walletType: Constants.keyWalletType, + canSend: true, + position: 4, + currencyBalance: ({amount: 999, + symbol: "USD", + displayDecimals: 2, + stripTrailingZeroes: false}), + migratedToKeycard: false + } + ] + + Component.onCompleted: append(data) + } + + WalletAccountsSelectorAdaptor { + id: walletAccountsSelectorAdaptor + + accounts: walletAccountsModel + assetsModel: d.assetsStore.groupedAccountAssetsModel + tokensBySymbolModel: d.assetsStore.walletTokensStore.plainTokensBySymbolModel + filteredFlatNetworksModel: d.filteredFlatNetworksModel + + selectedTokenKey: selectedTokenComboBox.currentValue + selectedNetworkChainId: networksComboBox.currentValue + + fnFormatCurrencyAmountFromBigInt: function(balance, symbol, decimals, options = null) { + return d.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, options) + } + } + + Item { + SplitView.preferredWidth: 150 + SplitView.fillHeight: true + ColumnLayout { + spacing: 16 + width: 150 + + WalletAccountsModel { + id: accountsModel + } + + Label { + text: "Default style" + font.bold: true + Layout.fillWidth: true + } + AccountSelector { + id: accountSelector + Layout.fillWidth: true + model: WalletAccountsModel {} + onCurrentAccountAddressChanged: { + accountSelector2.selectedAddress = currentAccountAddress + } + } + + Label { + text: "Header style" + font.bold: true + Layout.fillWidth: true + } + AccountSelectorHeader { + id: accountSelector2 + model: walletAccountsSelectorAdaptor.processedWalletAccounts + onCurrentAccountAddressChanged: { + accountSelector.selectedAddress = currentAccountAddress + } } } - Label { - text: "Header style" - font.bold: true - Layout.fillWidth: true - } - AccountSelectorHeader { - id: accountSelector2 - model: accountSelector.model - onCurrentAccountAddressChanged: { - accountSelector.selectedAddress = currentAccountAddress + } + + Item { + SplitView.preferredWidth: 300 + SplitView.preferredHeight: childrenRect.height + + ColumnLayout { + + Label { text: "Selected Token" } + ComboBox { + id: selectedTokenComboBox + textRole: "name" + valueRole: "key" + model: d.assetsStore.walletTokensStore.plainTokensBySymbolModel + currentIndex: -1 + } + + Label { text: "Selected Network" } + ComboBox { + id: networksComboBox + textRole: "chainName" + valueRole: "chainId" + model: d.filteredFlatNetworksModel + currentIndex: -1 } } - - } + } } // category: Components diff --git a/storybook/pages/SimpleSendModalPage.qml b/storybook/pages/SimpleSendModalPage.qml index 1e0d1be4b7..7f3f9c5564 100644 --- a/storybook/pages/SimpleSendModalPage.qml +++ b/storybook/pages/SimpleSendModalPage.qml @@ -1,11 +1,12 @@ import QtQuick 2.15 -import QtQuick.Layouts 1.15 import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 import SortFilterProxyModel 0.2 import StatusQ 0.1 import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 import StatusQ.Core.Backpressure 0.1 import Models 1.0 @@ -32,6 +33,10 @@ SplitView { readonly property WalletAssetsStore walletAssetStore: WalletAssetsStore { assetsWithFilteredBalances: groupedAccountsAssetsModel + walletTokensStore: TokensStore { + plainTokensBySymbolModel: TokensBySymbolModel{} + getDisplayAssetsBelowBalanceThresholdDisplayAmount: () => 0 + } } readonly property var walletAccountsModel: WalletAccountsModel{} @@ -67,6 +72,14 @@ SplitView { simpleSend.estimatedFiatFees = "1.45 EUR" simpleSend.estimatedCryptoFees = "0.0007 ETH" }) + + function formatCurrencyAmount(amount, symbol, options = null, locale = null) { + if (isNaN(amount)) { + return "N/A" + } + var currencyAmount = d.getCurrencyAmount(amount, symbol) + return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale) + } } PopupBackground { @@ -96,7 +109,7 @@ SplitView { interactive: interactiveCheckbox.checked - accountsModel: d.walletAccountsModel + accountsModel: accountsSelectorAdaptor.processedWalletAccounts assetsModel: assetsSelectorViewAdaptor.outputAssetsModel collectiblesModel: collectiblesSelectionAdaptor.model networksModel: d.filteredNetworksModel @@ -105,13 +118,7 @@ SplitView { recentRecipientsModel: WalletTransactionsModel{} currentCurrency: "USD" - fnFormatCurrencyAmount: function(amount, symbol, options = null, locale = null) { - if (isNaN(amount)) { - return "N/A" - } - var currencyAmount = d.getCurrencyAmount(amount, symbol) - return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale) - } + fnFormatCurrencyAmount: d.formatCurrencyAmount fnResolveENS: Backpressure.debounce(root, 500, function (ensName, uuid) { if (!!ensName && ensName.endsWith(".eth")) { @@ -146,6 +153,24 @@ SplitView { } } + WalletAccountsSelectorAdaptor { + id: accountsSelectorAdaptor + + accounts: d.walletAccountsModel + assetsModel: GroupedAccountsAssetsModel {} + tokensBySymbolModel: d.walletAssetStore.walletTokensStore.plainTokensBySymbolModel + filteredFlatNetworksModel: d.filteredNetworksModel + + selectedTokenKey: simpleSend.selectedTokenKey + selectedNetworkChainId: simpleSend.selectedChainId + + fnFormatCurrencyAmountFromBigInt: function(balance, symbol, decimals, options = null) { + let bigIntBalance = AmountsArithmetic.fromString(balance) + let decimalBalance = AmountsArithmetic.toNumber(bigIntBalance, decimals) + return d.formatCurrencyAmount(decimalBalance, symbol, options) + } + } + TokenSelectorViewAdaptor { id: assetsSelectorViewAdaptor diff --git a/storybook/pages/WalletAccountsSelectorAdaptorPage.qml b/storybook/pages/WalletAccountsSelectorAdaptorPage.qml new file mode 100644 index 0000000000..0cf252bc75 --- /dev/null +++ b/storybook/pages/WalletAccountsSelectorAdaptorPage.qml @@ -0,0 +1,187 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import SortFilterProxyModel 0.2 + +import AppLayouts.Wallet.stores 1.0 +import AppLayouts.Wallet.adaptors 1.0 + +import Storybook 1.0 +import Models 1.0 + +import shared.stores 1.0 +import utils 1.0 + +Item { + id: root + + ListModel { + id: walletAccountsModel + readonly property var data: [ + { + name: "helloworld", + address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", + emoji: "😋", + colorId: Constants.walletAccountColors.primary, + walletType: "", + canSend: true, + position: 0, + currencyBalance: ({amount: 1.25, + symbol: "USD", + displayDecimals: 2, + stripTrailingZeroes: false}), + migratedToKeycard: true + }, + { + name: "Hot wallet (generated)", + emoji: "🚗", + colorId: Constants.walletAccountColors.army, + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", + walletType: Constants.generatedWalletType, + canSend: true, + position: 3, + currencyBalance: ({amount: 10, + symbol: "USD", + displayDecimals: 2, + stripTrailingZeroes: false}), + migratedToKeycard: false + }, + { + name: "Family (seed)", + emoji: "🎨", + colorId: Constants.walletAccountColors.magenta, + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8882", + walletType: Constants.seedWalletType, + canSend: true, + position: 1, + currencyBalance: ({amount: 110.05, + symbol: "USD", + displayDecimals: 2, + stripTrailingZeroes: false}), + migratedToKeycard: false + }, + { + name: "Tag Heuer (watch)", + emoji: "⌚", + colorId: Constants.walletAccountColors.copper, + color: "#CB6256", + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8883", + walletType: Constants.watchWalletType, + canSend: false, + position: 2, + currencyBalance: ({amount: 3, + symbol: "USD", + displayDecimals: 2, + stripTrailingZeroes: false}), + migratedToKeycard: false + }, + { + name: "Fab (key)", + emoji: "🔑", + colorId: Constants.walletAccountColors.camel, + color: "#C78F67", + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8884", + walletType: Constants.keyWalletType, + canSend: true, + position: 4, + currencyBalance: ({amount: 999, + symbol: "USD", + displayDecimals: 2, + stripTrailingZeroes: false}), + migratedToKeycard: false + } + ] + + Component.onCompleted: append(data) + } + + QtObject { + id: d + + readonly property var assetsStore: WalletAssetsStore { + id: thisWalletAssetStore + walletTokensStore: TokensStore { + plainTokensBySymbolModel: TokensBySymbolModel {} + } + readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} + assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel + } + readonly property var currencyStore: CurrenciesStore{} + } + + WalletAccountsSelectorAdaptor { + id: adaptor + accounts: walletAccountsModel + assetsModel: d.assetsStore.groupedAccountAssetsModel + tokensBySymbolModel: d.assetsStore.walletTokensStore.plainTokensBySymbolModel + filteredFlatNetworksModel: SortFilterProxyModel { + sourceModel: NetworksModel.flatNetworks + filters: ValueFilter { roleName: "isTest"; value: true } + } + + fnFormatCurrencyAmountFromBigInt: function(balance, symbol, decimals, options = null) { + return d.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, options) + } + + selectedTokenKey: selectedTokenComboBox.currentValue + selectedNetworkChainId: networksComboBox.currentValue + } + + ColumnLayout { + anchors.fill: parent + + Label { text: "Selected Token" } + ComboBox { + id: selectedTokenComboBox + textRole: "name" + valueRole: "key" + model: d.assetsStore.walletTokensStore.plainTokensBySymbolModel + currentIndex: 0 + onCountChanged: currentIndex = 0 + } + + Label { text: "Selected Network" } + ComboBox { + id: networksComboBox + textRole: "chainName" + valueRole: "chainId" + model: adaptor.filteredFlatNetworksModel + currentIndex: 0 + onCountChanged: currentIndex = 0 + } + + RowLayout { + GenericListView { + label: "Input Accounts model" + + model: walletAccountsModel + + Layout.fillWidth: true + Layout.fillHeight: true + + roles: ["name", "address", "currencyBalance", "position", "canSend"] + + skipEmptyRoles: true + } + + GenericListView { + label: "Adapter's output model" + + model: adaptor.processedWalletAccounts + + Layout.fillWidth: true + Layout.fillHeight: true + + roles: ["name", "address", "currencyBalance", "position", "canSend", "accountBalance", "currencyBalanceDouble"] + + skipEmptyRoles: true + + insetComponent: Label { + text: "balance " + (model ? model.accountBalance.formattedBalance: "") + } + } + } + } +} + +// category: Adaptors diff --git a/storybook/qmlTests/tests/tst_SwapModal.qml b/storybook/qmlTests/tests/tst_SwapModal.qml index dbbd6200c4..a332236eae 100644 --- a/storybook/qmlTests/tests/tst_SwapModal.qml +++ b/storybook/qmlTests/tests/tst_SwapModal.qml @@ -171,26 +171,28 @@ Item { function test_floating_header_default_account() { verify(!!controlUnderTest) + const accountsModalHeader = getAndVerifyAccountsModalHeader() + let walletAccounts = accountsModalHeader.model /* using a for loop set different accounts as default index and check if the correct values are displayed in the floating header*/ - for (let i = 0; i< swapAdaptor.nonWatchAccounts.count; i++) { - const nonWatchAccount = swapAdaptor.nonWatchAccounts.get(i) - root.swapFormData.selectedAccountAddress = nonWatchAccount.address + for (let i = 0; i< walletAccounts.count; i++) { + const accountToTest = walletAccounts.get(i) + root.swapFormData.selectedAccountAddress = accountToTest.address // Launch popup launchAndVerfyModal() const floatingHeaderBackground = findChild(controlUnderTest, "headerBackground") verify(!!floatingHeaderBackground) - compare(floatingHeaderBackground.color.toString().toUpperCase(), Utils.getColorForId(nonWatchAccount.colorId).toString().toUpperCase()) + compare(floatingHeaderBackground.color.toString().toUpperCase(), Utils.getColorForId(accountToTest.colorId).toString().toUpperCase()) const headerContentItemText = findChild(controlUnderTest, "textContent") verify(!!headerContentItemText) - compare(headerContentItemText.text, nonWatchAccount.name) + compare(headerContentItemText.text, accountToTest.name) const headerContentItemEmoji = findChild(controlUnderTest, "assetContent") verify(!!headerContentItemEmoji) - compare(headerContentItemEmoji.asset.emoji, nonWatchAccount.emoji) + compare(headerContentItemEmoji.asset.emoji, accountToTest.emoji) } closeAndVerfyModal() } @@ -228,6 +230,7 @@ Item { launchAndVerfyModal() const accountsModalHeader = getAndVerifyAccountsModalHeader() launchAccountSelectionPopup(accountsModalHeader) + let walletAccounts = accountsModalHeader.model const comboBoxList = findChild(controlUnderTest, "accountSelectorList") verify(!!comboBoxList) @@ -235,7 +238,7 @@ Item { for(let i =0; i< comboBoxList.model.count; i++) { let delegateUnderTest = comboBoxList.itemAtIndex(i) - let accountToBeTested = swapAdaptor.nonWatchAccounts.get(i) + let accountToBeTested = walletAccounts.get(i) let elidedAddress = SQUtils.Utils.elideAndFormatWalletAddress(accountToBeTested.address) compare(delegateUnderTest.title, accountToBeTested.name) compare(delegateUnderTest.subTitle, elidedAddress) @@ -302,7 +305,6 @@ Item { for(let i =0; i< comboBoxList.model.count; i++) { let delegateUnderTest = comboBoxList.itemAtIndex(i) - verify(!!delegateUnderTest.model.fromToken) verify(!!delegateUnderTest.model.accountBalance) compare(delegateUnderTest.inlineTagModel, 1) @@ -315,9 +317,9 @@ Item { compare(inlineTagDelegate_0.asset.color.toString().toUpperCase(), delegateUnderTest.model.accountBalance.chainColor.toString().toUpperCase()) compare(inlineTagDelegate_0.titleText.color, balance === "0" ? Theme.palette.baseColor1 : Theme.palette.directColor1) - let bigIntBalance = SQUtils.AmountsArithmetic.toNumber(balance, delegateUnderTest.model.fromToken.decimals) - compare(inlineTagDelegate_0.title, balance === "0" ? "0 %1".arg(delegateUnderTest.model.fromToken.symbol) - : root.swapAdaptor.formatCurrencyAmount(bigIntBalance, delegateUnderTest.model.fromToken.symbol)) + let bigIntBalance = SQUtils.AmountsArithmetic.toNumber(balance, controlUnderTest.swapAdaptor.fromToken.decimals) + compare(inlineTagDelegate_0.title, balance === "0" ? "0 %1".arg(controlUnderTest.swapAdaptor.fromToken.symbol) + : root.swapAdaptor.currencyStore.formatCurrencyAmount(bigIntBalance, controlUnderTest.swapAdaptor.fromToken.symbol)) } closeAndVerfyModal() @@ -327,13 +329,16 @@ Item { // Launch popup launchAndVerfyModal() + const accountsModalHeader = getAndVerifyAccountsModalHeader() + let walletAccounts = accountsModalHeader.model + const payPanel = findChild(controlUnderTest, "payPanel") verify(!!payPanel) const amountToSendInput = findChild(payPanel, "amountToSendInput") verify(!!amountToSendInput) verify(amountToSendInput.cursorVisible) - for(let i =0; i< swapAdaptor.nonWatchAccounts.count; i++) { + for(let i =0; i< walletAccounts.count; i++) { // launch account selection dropdown const accountsModalHeader = getAndVerifyAccountsModalHeader() launchAccountSelectionPopup(accountsModalHeader) @@ -348,20 +353,20 @@ Item { verify(accountsModalHeader.control.popup.closed) // The input params form's slected Index should be updated as per this selection - compare(root.swapFormData.selectedAccountAddress, swapAdaptor.nonWatchAccounts.get(i).address) + compare(root.swapFormData.selectedAccountAddress, walletAccounts.get(i).address) // The comboBox item should reflect chosen account const floatingHeaderBackground = findChild(accountsModalHeader, "headerBackground") verify(!!floatingHeaderBackground) - compare(floatingHeaderBackground.color.toString().toUpperCase(), swapAdaptor.nonWatchAccounts.get(i).color.toString().toUpperCase()) + compare(floatingHeaderBackground.color.toString().toUpperCase(), walletAccounts.get(i).color.toString().toUpperCase()) const headerContentItemText = findChild(accountsModalHeader, "textContent") verify(!!headerContentItemText) - compare(headerContentItemText.text, swapAdaptor.nonWatchAccounts.get(i).name) + compare(headerContentItemText.text, walletAccounts.get(i).name) const headerContentItemEmoji = findChild(accountsModalHeader, "assetContent") verify(!!headerContentItemEmoji) - compare(headerContentItemEmoji.asset.emoji, swapAdaptor.nonWatchAccounts.get(i).emoji) + compare(headerContentItemEmoji.asset.emoji, walletAccounts.get(i).emoji) waitForRendering(amountToSendInput) @@ -477,7 +482,7 @@ Item { verify(!!fromToken) let bigIntBalance = SQUtils.AmountsArithmetic.toNumber(accountBalance.balance, fromToken.decimals) compare(inlineTagDelegate_0.title, bigIntBalance === 0 ? "0 %1".arg(fromToken.symbol) - : root.swapAdaptor.formatCurrencyAmount(bigIntBalance, fromToken.symbol)) + : root.swapAdaptor.currencyStore.formatCurrencyAmount(bigIntBalance, fromToken.symbol)) } // close account selection dropdown accountsModalHeader.control.popup.close() @@ -918,7 +923,11 @@ Item { // try setting value before popup is launched and check values let valueToExchange = 0.001 let valueToExchangeString = valueToExchange.toString() - root.swapFormData.selectedAccountAddress = swapAdaptor.nonWatchAccounts.get(0).address + + const accountsModalHeader = getAndVerifyAccountsModalHeader() + let walletAccounts = accountsModalHeader.model + + root.swapFormData.selectedAccountAddress = walletAccounts.get(0).address root.swapFormData.selectedNetworkChainId = root.swapAdaptor.filteredFlatNetworksModel.get(0).chainId root.swapFormData.fromTokensKey = "ETH" root.swapFormData.fromTokenAmount = valueToExchangeString @@ -968,11 +977,14 @@ Item { } function test_modal_pay_input_wrong_value_1() { + const accountsModalHeader = getAndVerifyAccountsModalHeader() + let walletAccounts = accountsModalHeader.model + let invalidValues = ["ABC", "0.0.010201", "12PASA", "100,9.01"] for (let i =0; i unique string ID of the token (asset); e.g. "ETH" or contract address + - name: string -> user visible token name (e.g. "Ethereum") + - symbol: string -> user visible token symbol (e.g. "ETH") + - decimals: int -> number of decimal places + - communityId: string -> optional; ID of the community this token belongs to, if any + - marketDetails: var -> object containing props like `currencyPrice` for the computed values below + - balances: submodel -> [ chainId:int, account:string, balance:BigIntString, iconUrl:string ] + */ + required property var assetsModel + /** Expected token by symbol model structure: + - key: id for the token, + - name: name of the token, + - symbol: symbol of the token, + - decimals: decimals for the token + */ + required property var tokensBySymbolModel + /** Expected networks model structure: + - chainId: chain Id for network, + - chainName: name of network, + - iconUrl: icon representing the network, + */ + required property var filteredFlatNetworksModel + /** selectedTokenKey: + the selected token key + */ + required property string selectedTokenKey + /** selectedNetworkChainId: + the selected network chainId + */ + required property int selectedNetworkChainId + + /** function to calculate token balance from BigInt: + the selected network chainId + */ + required property var fnFormatCurrencyAmountFromBigInt + + /** output model + Computed processedWalletAccounts model addon values: + - accountBalance: balance of selected token on selected network along with network information + - filters out account that cant be used to send + */ + readonly property var processedWalletAccounts: SortFilterProxyModel { + sourceModel: root.accounts + delayed: true // Delayed to allow `processAccountBalance` dependencies to be resolved + filters: ValueFilter { + roleName: "canSend" + value: true + } + sorters: [ + RoleSorter { roleName: "currencyBalanceDouble"; sortOrder: Qt.DescendingOrder }, + RoleSorter { roleName: "position"; sortOrder: Qt.AscendingOrder } + ] + proxyRoles: [ + FastExpressionRole { + name: "accountBalance" + expression: { + // dependencies + root.selectedTokenKey + root.selectedNetworkChainId + return d.processAccountBalance(model.address) + } + expectedRoles: ["address"] + }, + FastExpressionRole { + name: "currencyBalanceDouble" + expression: model.currencyBalance.amount + expectedRoles: ["currencyBalance"] + } + ] + } + + QtObject { + id: d + + readonly property ObjectProxyModel filteredBalancesModel: ObjectProxyModel { + sourceModel: root.assetsModel + + delegate: SortFilterProxyModel { + readonly property var balances: this + + sourceModel: LeftJoinModel { + leftModel: model.balances + rightModel: root.filteredFlatNetworksModel + + joinRole: "chainId" + } + + filters: ValueFilter { + roleName: "chainId" + value: root.selectedNetworkChainId + } + } + + expectedRoles: "balances" + exposedRoles: "balances" + } + + function processAccountBalance(address) { + let selectedToken = ModelUtils.getByKey(root.tokensBySymbolModel, "key", root.selectedTokenKey) + if (!selectedToken) { + return null + } + + let network = ModelUtils.getByKey(root.filteredFlatNetworksModel, "chainId", root.selectedNetworkChainId) + if (!network) { + return null + } + + let balancesModel = ModelUtils.getByKey(filteredBalancesModel, "tokensKey", root.selectedTokenKey, "balances") + let accountBalance = ModelUtils.getByKey(balancesModel, "account", address) + if(accountBalance && accountBalance.balance !== "0") { + accountBalance.formattedBalance = root.fnFormatCurrencyAmountFromBigInt(accountBalance.balance, selectedToken.symbol, selectedToken.decimals) + return accountBalance + } + + return { + balance: "0", + iconUrl: network.iconUrl, + chainColor: network.chainColor, + formattedBalance: "0 %1".arg(selectedToken.symbol) + } + } + } +} diff --git a/ui/app/AppLayouts/Wallet/adaptors/qmldir b/ui/app/AppLayouts/Wallet/adaptors/qmldir index ed8eee3848..cc07adb9f3 100644 --- a/ui/app/AppLayouts/Wallet/adaptors/qmldir +++ b/ui/app/AppLayouts/Wallet/adaptors/qmldir @@ -1,2 +1,3 @@ CollectiblesSelectionAdaptor 1.0 CollectiblesSelectionAdaptor.qml TokenSelectorViewAdaptor 1.0 TokenSelectorViewAdaptor.qml +WalletAccountsSelectorAdaptor 1.0 WalletAccountsSelectorAdaptor.qml diff --git a/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml b/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml index 73b3f6a311..ffa6f3a092 100644 --- a/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml @@ -23,9 +23,6 @@ StatusDialog { id: root /** - TODO: use the newly defined WalletAccountsSelectorAdaptor - in https://github.com/status-im/status-desktop/pull/16834 - This will also remove watch only accounts from the list Expected model structure: - name: name of account - address: wallet address diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml index f78e34d05d..227e07d5d6 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml @@ -1,9 +1,11 @@ import QtQuick 2.15 import QtQuick.Layouts 1.15 import QtQml.Models 2.15 +import SortFilterProxyModel 0.2 import utils 1.0 +import StatusQ 0.1 import StatusQ.Controls 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Backpressure 0.1 @@ -18,6 +20,7 @@ import shared.panels 1.0 import AppLayouts.Wallet.controls 1.0 import AppLayouts.Wallet.panels 1.0 import AppLayouts.Wallet.popups.buy 1.0 +import AppLayouts.Wallet.adaptors 1.0 StatusDialog { id: root @@ -58,11 +61,37 @@ StatusDialog { selectedTokenKey: root.swapInputParamsForm.fromTokensKey } + readonly property WalletAccountsSelectorAdaptor accountsSelectorAdaptor : WalletAccountsSelectorAdaptor { + accounts: root.swapAdaptor.swapStore.accounts + assetsModel: root.swapAdaptor.walletAssetsStore.baseGroupedAccountAssetModel + tokensBySymbolModel: root.swapAdaptor.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel + filteredFlatNetworksModel: SortFilterProxyModel { + sourceModel: root.swapAdaptor.swapStore.flatNetworks + filters: ValueFilter { roleName: "isTest"; value: root.swapAdaptor.swapStore.areTestNetworksEnabled } + } + + selectedTokenKey: root.swapInputParamsForm.fromTokensKey + selectedNetworkChainId: root.swapInputParamsForm.selectedNetworkChainId + + fnFormatCurrencyAmountFromBigInt: function(balance, symbol, decimals, options = null) { + return root.swapAdaptor.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, options) + } + } + + readonly property var selectedAccount: selectedAccountEntry.item + function addMetricsEvent(subEventName) { Global.addCentralizedMetricIfEnabled("swap", {subEvent: subEventName}) } } + ModelEntry { + id: selectedAccountEntry + sourceModel: d.accountsSelectorAdaptor.processedWalletAccounts + key: "address" + value: root.swapInputParamsForm.selectedAccountAddress + } + Connections { target: root.swapInputParamsForm function onFormValuesChanged() { @@ -109,7 +138,7 @@ StatusDialog { AccountSelectorHeader { id: selector control.popup.width: 512 - model: root.swapAdaptor.nonWatchAccounts + model: d.accountsSelectorAdaptor.processedWalletAccounts selectedAddress: root.swapInputParamsForm.selectedAccountAddress onCurrentAccountAddressChanged: { if (currentAccountAddress !== "" && currentAccountAddress !== root.swapInputParamsForm.selectedAccountAddress) { @@ -409,7 +438,7 @@ StatusDialog { objectName: "signButton" readonly property string fromTokenSymbol: !!root.swapAdaptor.fromToken ? root.swapAdaptor.fromToken.symbol ?? "" : "" loadingWithText: root.swapAdaptor.approvalPending - icon.name: root.swapAdaptor.selectedAccount.migratedToKeycard ? Constants.authenticationIconByType[Constants.LoginType.Keycard] + icon.name: d.selectedAccount.migratedToKeycard ? Constants.authenticationIconByType[Constants.LoginType.Keycard] : Constants.authenticationIconByType[root.loginType] text: { if(root.swapAdaptor.validSwapProposalReceived) { @@ -461,7 +490,7 @@ StatusDialog { formatBigNumber: (number, symbol, noSymbolOption) => root.swapAdaptor.currencyStore.formatBigNumber(number, symbol, noSymbolOption) - loginType: root.swapAdaptor.selectedAccount.migratedToKeycard ? Constants.LoginType.Keycard : root.loginType + loginType: d.selectedAccount.migratedToKeycard ? Constants.LoginType.Keycard : root.loginType feesLoading: root.swapAdaptor.swapProposalLoading fromTokenSymbol: root.swapAdaptor.fromToken.symbol @@ -470,11 +499,11 @@ StatusDialog { "chainId", root.swapInputParamsForm.selectedNetworkChainId, "address") - accountName: root.swapAdaptor.selectedAccount.name - accountAddress: root.swapAdaptor.selectedAccount.address - accountEmoji: root.swapAdaptor.selectedAccount.emoji - accountColor: Utils.getColorForId(root.swapAdaptor.selectedAccount.colorId) - accountBalanceFormatted: root.swapAdaptor.selectedAccount.accountBalance.formattedBalance + accountName: d.selectedAccount.name + accountAddress: d.selectedAccount.address + accountEmoji: d.selectedAccount.emoji + accountColor: Utils.getColorForId(d.selectedAccount.colorId) + accountBalanceFormatted: d.selectedAccount.accountBalance.formattedBalance networkShortName: networkFilter.singleSelectionItemData.shortName networkName: networkFilter.singleSelectionItemData.chainName @@ -513,7 +542,7 @@ StatusDialog { formatBigNumber: (number, symbol, noSymbolOption) => root.swapAdaptor.currencyStore.formatBigNumber(number, symbol, noSymbolOption) - loginType: root.swapAdaptor.selectedAccount.migratedToKeycard ? Constants.LoginType.Keycard : root.loginType + loginType: d.selectedAccount.migratedToKeycard ? Constants.LoginType.Keycard : root.loginType feesLoading: root.swapAdaptor.swapProposalLoading fromTokenSymbol: root.swapAdaptor.fromToken.symbol @@ -528,10 +557,10 @@ StatusDialog { "chainId", root.swapInputParamsForm.selectedNetworkChainId, "address") - accountName: root.swapAdaptor.selectedAccount.name - accountAddress: root.swapAdaptor.selectedAccount.address - accountEmoji: root.swapAdaptor.selectedAccount.emoji - accountColor: Utils.getColorForId(root.swapAdaptor.selectedAccount.colorId) + accountName: d.selectedAccount.name + accountAddress: d.selectedAccount.address + accountEmoji: d.selectedAccount.emoji + accountColor: Utils.getColorForId(d.selectedAccount.colorId) networkShortName: networkFilter.singleSelectionItemData.shortName networkName: networkFilter.singleSelectionItemData.chainName diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml index b54fe7e549..7a8104aa13 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml @@ -33,51 +33,9 @@ QObject { // To expose the selected from and to Token from the SwapModal readonly property var fromToken: fromTokenEntry.item readonly property var toToken: toTokenEntry.item - readonly property var selectedAccount: selectedAccountEntry.item readonly property string uuid: d.uuid - // TO REVIEW: Handle this in a separate `WalletAccountsAdaptor.qml` file. - // Probably this data transformation should live there since they have common base. - readonly property var nonWatchAccounts: SortFilterProxyModel { - sourceModel: root.swapStore.accounts - delayed: true // Delayed to allow `processAccountBalance` dependencies to be resolved - filters: ValueFilter { - roleName: "canSend" - value: true - } - sorters: [ - RoleSorter { roleName: "currencyBalanceDouble"; sortOrder: Qt.DescendingOrder }, - RoleSorter { roleName: "position"; sortOrder: Qt.AscendingOrder } - ] - proxyRoles: [ - FastExpressionRole { - name: "accountBalance" - expression: { - // dependencies - root.swapFormData.fromTokensKey - root.fromToken - root.fromToken.symbol - root.fromToken.decimals - root.swapFormData.selectedNetworkChainId - root.swapFormData.fromTokensKey - - return d.processAccountBalance(model.address) - } - expectedRoles: ["address"] - }, - FastExpressionRole { - name: "currencyBalanceDouble" - expression: model.currencyBalance.amount - expectedRoles: ["currencyBalance"] - }, - FastExpressionRole { - name: "fromToken" - expression: root.fromToken - } - ] - } - readonly property SortFilterProxyModel filteredFlatNetworksModel: SortFilterProxyModel { sourceModel: root.swapStore.flatNetworks filters: ValueFilter { roleName: "isTest"; value: root.swapStore.areTestNetworksEnabled } @@ -94,55 +52,6 @@ QObject { // storing txHash to verify against tx completed event property string txHash - readonly property ObjectProxyModel filteredBalancesModel: ObjectProxyModel { - sourceModel: root.walletAssetsStore.baseGroupedAccountAssetModel - - delegate: SortFilterProxyModel { - readonly property var balances: this - - sourceModel: LeftJoinModel { - leftModel: model.balances - rightModel: root.swapStore.flatNetworks - - joinRole: "chainId" - } - - filters: ValueFilter { - roleName: "chainId" - value: root.swapFormData.selectedNetworkChainId - } - } - - expectedRoles: "balances" - exposedRoles: "balances" - } - - function processAccountBalance(address) { - if (!root.swapFormData.fromTokensKey || !root.fromToken) { - return null - } - - let network = ModelUtils.getByKey(root.filteredFlatNetworksModel, "chainId", root.swapFormData.selectedNetworkChainId) - - if (!network) { - return null - } - - let balancesModel = ModelUtils.getByKey(filteredBalancesModel, "tokensKey", root.swapFormData.fromTokensKey, "balances") - let accountBalance = ModelUtils.getByKey(balancesModel, "account", address) - if(accountBalance && accountBalance.balance !== "0") { - accountBalance.formattedBalance = root.formatCurrencyAmountFromBigInt(accountBalance.balance, root.fromToken.symbol, root.fromToken.decimals) - return accountBalance - } - - return { - balance: "0", - iconUrl: network.iconUrl, - chainColor: network.chainColor, - formattedBalance: "0 %1".arg(root.fromToken.symbol) - } - } - // Properties to handle error states readonly property bool isRouteEthBalanceInsufficient: root.validSwapProposalReceived && root.swapOutputData.errCode === Constants.routerErrorCodes.router.errNotEnoughNativeBalance @@ -209,13 +118,6 @@ QObject { value: root.swapFormData.toTokenKey } - ModelEntry { - id: selectedAccountEntry - sourceModel: root.nonWatchAccounts - key: "address" - value: root.swapFormData.selectedAccountAddress - } - Connections { target: root.swapStore function onSuggestedRoutesReady(txRoutes, errCode, errDescription) { @@ -284,14 +186,6 @@ QObject { d.txHash = "" } - function formatCurrencyAmount(balance, symbol, options = null, locale = null) { - return root.currencyStore.formatCurrencyAmount(balance, symbol, options, locale) - } - - function formatCurrencyAmountFromBigInt(balance, symbol, decimals, options = null) { - return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, options) - } - function getDisabledChainIds(enabledChainId) { let disabledChainIds = [] let chainIds = ModelUtils.modelToFlatArray(root.filteredFlatNetworksModel, "chainId") diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 1fc0691c86..d6290b2757 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -668,21 +668,22 @@ Item { flatNetworksModel: WalletStores.RootStore.flatNetworks areTestNetworksEnabled: WalletStores.RootStore.areTestNetworksEnabled groupedAccountAssetsModel: appMain.walletAssetsStore.groupedAccountAssetsModel - currentCurrency: appMain.currencyStore.currentCurrency + plainTokensBySymbolModel: appMain.tokensStore.plainTokensBySymbolModel showCommunityAssetsInSend: appMain.tokensStore.showCommunityAssetsInSend collectiblesBySymbolModel: WalletStores.RootStore.collectiblesStore.jointCollectiblesBySymbolModel tokenBySymbolModel: appMain.tokensStore.plainTokensBySymbolModel - fnFormatCurrencyAmount: function(amount, symbol, options = null, locale = null) { - return appMain.currencyStore.formatCurrencyAmount(amount, symbol) - } + savedAddressesModel: WalletStores.RootStore.savedAddresses + recentRecipientsModel: appMain.transactionStore.tempActivityController1Model + + currentCurrency: appMain.currencyStore.currentCurrency + fnFormatCurrencyAmount: appMain.currencyStore.formatCurrencyAmount + fnFormatCurrencyAmountFromBigInt: appMain.currencyStore.formatCurrencyAmountFromBigInt + // TODO remove this call to mainModule under #16919 fnResolveENS: function(ensName, uuid) { mainModule.resolveENS(name, uuid) } - savedAddressesModel: WalletStores.RootStore.savedAddresses - recentRecipientsModel: appMain.transactionStore.tempActivityController1Model - Component.onCompleted: { // It's requested from many nested places, so as a workaround we use // Global to shorten the path via global signal. diff --git a/ui/app/mainui/SendModalHandler.qml b/ui/app/mainui/SendModalHandler.qml index 37ebc558b4..49eee7d012 100644 --- a/ui/app/mainui/SendModalHandler.qml +++ b/ui/app/mainui/SendModalHandler.qml @@ -41,8 +41,6 @@ QtObject { /** For simple send modal flows, decoupling from transaction store **/ - /** curently selected fiat currency symbol **/ - required property string currentCurrency /** Expected model structure: - name: name of account - address: wallet address @@ -62,6 +60,13 @@ QtObject { - balances: submodel[ chainId:int, account:string, balance:BigIntString, iconUrl:string ] **/ required property var groupedAccountAssetsModel + /** Expected token by symbol model structure: + - key: id for the token, + - name: name of the token, + - symbol: symbol of the token, + - decimals: decimals for the token + */ + required property var plainTokensBySymbolModel /** Expected model structure: - symbol [string] - unique identifier of a collectible - collectionUid [string] - unique identifier of a collection @@ -96,8 +101,6 @@ QtObject { /** whether community tokens are shown in send modal based on a global setting **/ required property bool showCommunityAssetsInSend - /** required function to format currency amount to locale string **/ - required property var fnFormatCurrencyAmount required property var savedAddressesModel required property var recentRecipientsModel @@ -107,6 +110,13 @@ QtObject { /** required signal to receive resolved ens name address **/ signal ensNameResolved(string resolvedPubKey, string resolvedAddress, string uuid) + /** curently selected fiat currency symbol **/ + required property string currentCurrency + /** required function to format currency amount to locale string **/ + required property var fnFormatCurrencyAmount + /** required function to format to currency amount from big int **/ + required property var fnFormatCurrencyAmountFromBigInt + function openSend(params = {}) { // TODO remove once simple send is feature complete let sendModalCmp = root.simpleSendEnabled ? simpleSendModalComponent: sendModalComponent @@ -243,14 +253,13 @@ QtObject { SimpleSendModal { id: simpleSendModal - /** TODO: use the newly defined WalletAccountsSelectorAdaptor - in https://github.com/status-im/status-desktop/pull/16834 **/ - accountsModel: root.walletAccountsModel + accountsModel: backendHandler.accountsSelectorAdaptor.processedWalletAccounts assetsModel: backendHandler.assetsSelectorViewAdaptor.outputAssetsModel collectiblesModel: backendHandler.collectiblesSelectionAdaptor.model networksModel: backendHandler.filteredFlatNetworksModel savedAddressesModel: root.savedAddressesModel recentRecipientsModel: root.recentRecipientsModel + currentCurrency: root.currentCurrency fnFormatCurrencyAmount: root.fnFormatCurrencyAmount fnResolveENS: root.fnResolveENS @@ -315,6 +324,18 @@ QtObject { close() } + readonly property var accountsSelectorAdaptor: WalletAccountsSelectorAdaptor { + accounts: root.walletAccountsModel + assetsModel: root.groupedAccountAssetsModel + tokensBySymbolModel: root.plainTokensBySymbolModel + filteredFlatNetworksModel: backendHandler.filteredFlatNetworksModel + + selectedTokenKey: simpleSendModal.selectedTokenKey + selectedNetworkChainId: simpleSendModal.selectedChainId + + fnFormatCurrencyAmountFromBigInt: root.fnFormatCurrencyAmountFromBigInt + } + readonly property var assetsSelectorViewAdaptor: TokenSelectorViewAdaptor { // TODO: remove all store dependecies and add specific properties to the handler instead assetsModel: root.groupedAccountAssetsModel