diff --git a/storybook/pages/SwapInputPanelPage.qml b/storybook/pages/SwapInputPanelPage.qml index e44d19532..60fb12454 100644 --- a/storybook/pages/SwapInputPanelPage.qml +++ b/storybook/pages/SwapInputPanelPage.qml @@ -30,6 +30,7 @@ SplitView { id: d readonly property SwapInputParamsForm swapInputParamsForm: SwapInputParamsForm { + selectedNetworkChainId: ctrlSelectedNetworkChainId.currentValue fromTokensKey: ctrlFromTokensKey.text fromTokenAmount: ctrlFromTokenAmount.text toTokenKey: ctrlToTokenKey.text @@ -74,7 +75,7 @@ SplitView { } currencyStore: d.adaptor.currencyStore - flatNetworksModel: d.adaptor.filteredFlatNetworksModel + flatNetworksModel: d.adaptor.swapStore.flatNetworks processedAssetsModel: d.adaptor.processedAssetsModel tokenKey: d.swapInputParamsForm.fromTokensKey @@ -125,6 +126,22 @@ SplitView { ColumnLayout { anchors.fill: parent + RowLayout { + Layout.fillWidth: true + Label { + text: "Chain:" + } + ComboBox { + Layout.fillWidth: true + id: ctrlSelectedNetworkChainId + model: d.adaptor.swapStore.flatNetworks + textRole: "chainName" + valueRole: "chainId" + displayText: currentIndex === -1 ? "All chains" : currentText + currentIndex: -1 // all chains + } + } + RowLayout { Layout.fillWidth: true Label { diff --git a/storybook/pages/SwapModalPage.qml b/storybook/pages/SwapModalPage.qml index b7d8cf10e..7cf8ce14b 100644 --- a/storybook/pages/SwapModalPage.qml +++ b/storybook/pages/SwapModalPage.qml @@ -65,19 +65,9 @@ SplitView { id: swapInputForm selectedAccountIndex: accountComboBox.currentIndex selectedNetworkChainId: d.getNetwork() - fromTokensKey: { - if (d.tokenBySymbolModel.count > 0) { - return ModelUtils.get(d.tokenBySymbolModel, fromTokenComboBox.currentIndex, "key") - } - return "" - } + fromTokensKey: fromTokenComboBox.currentValue fromTokenAmount: swapInput.text - toTokenKey: { - if (d.tokenBySymbolModel.count > 0) { - return ModelUtils.get(d.tokenBySymbolModel, toTokenComboBox.currentIndex, "key") - } - return "" - } + toTokenKey: toTokenComboBox.currentValue toTokenAmount: swapOutputAmount.text } @@ -87,33 +77,34 @@ SplitView { visible: true modal: false closePolicy: Popup.CloseOnEscape + destroyOnClose: true swapInputParamsForm: swapInputForm - swapAdaptor: SwapModalAdaptor { - swapProposalLoading: loadingCheckBox.checked - swapProposalReady: swapProposalReadyCheckBox.checked - swapStore: SwapStore { - readonly property var accounts: d.accountsModel - readonly property var flatNetworks: d.flatNetworksModel - readonly property bool areTestNetworksEnabled: areTestNetworksEnabledCheckbox.checked + swapAdaptor: SwapModalAdaptor { + swapProposalLoading: loadingCheckBox.checked + swapProposalReady: swapProposalReadyCheckBox.checked + swapStore: SwapStore { + readonly property var accounts: d.accountsModel + readonly property var flatNetworks: d.flatNetworksModel + readonly property bool areTestNetworksEnabled: areTestNetworksEnabledCheckbox.checked - signal suggestedRoutesReady(var txRoutes) + signal suggestedRoutesReady(var txRoutes) - function fetchSuggestedRoutes(accountFrom, accountTo, amount, tokenFrom, tokenTo, - disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts) {} - function authenticateAndTransfer(uuid, accountFrom, accountTo, - tokenFrom, tokenTo, sendType, tokenName, tokenIsOwnerToken, paths) {} - } - walletAssetsStore: WalletAssetsStore { - id: thisWalletAssetStore - walletTokensStore: TokensStore { - readonly property var plainTokensBySymbolModel: TokensBySymbolModel {} - } - readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} - assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel - } - currencyStore: CurrenciesStore {} - swapFormData: swapInputForm + function fetchSuggestedRoutes(accountFrom, accountTo, amount, tokenFrom, tokenTo, + disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts) {} + function authenticateAndTransfer(uuid, accountFrom, accountTo, + tokenFrom, tokenTo, sendType, tokenName, tokenIsOwnerToken, paths) {} } + walletAssetsStore: WalletAssetsStore { + id: thisWalletAssetStore + walletTokensStore: TokensStore { + plainTokensBySymbolModel: TokensBySymbolModel {} + } + readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} + assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel + } + currencyStore: CurrenciesStore {} + swapFormData: swapInputForm + } } } } @@ -131,7 +122,7 @@ SplitView { id: areTestNetworksEnabledCheckbox text: "areTestNetworksEnabled" checked: true - onCheckedChanged: networksComboBox.currentIndex = 0 + onToggled: networksComboBox.currentIndex = 0 } StatusBaseText { @@ -173,6 +164,7 @@ SplitView { ComboBox { id: fromTokenComboBox textRole: "name" + valueRole: "key" model: d.tokenBySymbolModel currentIndex: 0 } @@ -190,6 +182,7 @@ SplitView { ComboBox { id: toTokenComboBox textRole: "name" + valueRole: "key" model: d.tokenBySymbolModel currentIndex: 1 } @@ -197,7 +190,7 @@ SplitView { StatusInput { id: swapOutputAmount Layout.preferredWidth: 100 - label: "Token amount to receive" + label: "Token amount to receive" text: "100" } diff --git a/storybook/pages/TokenSelectorAssetDelegatePage.qml b/storybook/pages/TokenSelectorAssetDelegatePage.qml new file mode 100644 index 000000000..2b5831340 --- /dev/null +++ b/storybook/pages/TokenSelectorAssetDelegatePage.qml @@ -0,0 +1,97 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 + +import Storybook 1.0 +import Models 1.0 + +import SortFilterProxyModel 0.2 + +import AppLayouts.Wallet.views 1.0 + +SplitView { + id: root + orientation: Qt.Vertical + + Logs { id: logs } + + Pane { + SplitView.fillWidth: true + SplitView.fillHeight: true + + background: Rectangle { + color: Theme.palette.baseColor3 + } + + Rectangle { + width: 380 + height: 200 + color: Theme.palette.statusListItem.backgroundColor + border.color: Theme.palette.primaryColor1 + border.width: 1 + anchors.centerIn: parent + + TokenSelectorAssetDelegate { + implicitWidth: 333 + anchors.centerIn: parent + + tokensKey: "ETH" + name: "Ethereum" + symbol: "ETH" + currencyBalanceAsString: "14,456.42 USD" + balancesModel: ListModel { + readonly property var data: [ + { chainId: 1, balanceAsString: "1234.50", iconUrl: "network/Network=Ethereum" }, + { chainId: 42161, balanceAsString: "55.91", iconUrl: "network/Network=Arbitrum" }, + { chainId: 10, balanceAsString: "45.12", iconUrl: "network/Network=Optimism" }, + { chainId: 420, balanceAsString: "1.23", iconUrl: "network/Network=Testnet" } + ] + Component.onCompleted: append(data) + } + + interactive: ctrlInteractive.checked + highlighted: ctrlHighlighted.checked + + onAssetSelected: (tokensKey) => { + console.warn("!!! TOKEN SELECTED:", tokensKey) + logs.logEvent("TokenSelectorAssetDelegate::onTokenSelected", ["tokensKey"], arguments) + } + } + } + } + + LogsAndControlsPanel { + SplitView.minimumHeight: 300 + SplitView.preferredHeight: 300 + + logsView.logText: logs.logText + + RowLayout { + anchors.fill: parent + + ColumnLayout { + Switch { + id: ctrlInteractive + text: "Interactive" + checked: true + } + Switch { + id: ctrlHighlighted + text: "Highlighted" + checked: false + } + + Item { Layout.fillHeight: true } + } + } + } +} + +// category: Delegates diff --git a/storybook/pages/TokenSelectorViewPage.qml b/storybook/pages/TokenSelectorViewPage.qml new file mode 100644 index 000000000..26d7f9f12 --- /dev/null +++ b/storybook/pages/TokenSelectorViewPage.qml @@ -0,0 +1,206 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 + +import Storybook 1.0 +import Models 1.0 + +import SortFilterProxyModel 0.2 + +import AppLayouts.Wallet.views 1.0 +import AppLayouts.Wallet.stores 1.0 +import AppLayouts.Wallet.adaptors 1.0 + +import shared.stores 1.0 +import utils 1.0 + +SplitView { + id: root + orientation: Qt.Vertical + + Logs { id: logs } + + QtObject { + id: d + + property var enabledChainIds: [] + function addFilter(chainId) { + if (d.enabledChainIds.includes(chainId)) + return + const newFilters = d.enabledChainIds.concat(chainId) + d.enabledChainIds = newFilters + } + function removeFilter(chainId) { + const newFilters = d.enabledChainIds.filter((filter) => filter !== chainId) + d.enabledChainIds = newFilters + } + function rebuildFilter() { + let newFilters = [] + for (let i = 0; i < chainIdsRepeater.count; i++) { + const item = chainIdsRepeater.itemAt(i) + if (!!item && item.checked) { + newFilters.push(item.chainId) + } + } + d.enabledChainIds = newFilters + } + + readonly property string enabledChainIdsString: enabledChainIds.join(":") + + readonly property var flatNetworks: NetworksModel.flatNetworks + readonly property var currencyStore: CurrenciesStore {} + readonly property var assetsStore: WalletAssetsStore { + id: thisWalletAssetStore + walletTokensStore: TokensStore { + plainTokensBySymbolModel: TokensBySymbolModel {} + } + readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} + assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel + } + + readonly property var walletAccountsModel: WalletAccountsModel {} + + readonly property var adaptor: TokenSelectorViewAdaptor { + assetsModel: d.assetsStore.groupedAccountAssetsModel + flatNetworksModel: d.flatNetworks + enabledChainIds: d.enabledChainIds + currentCurrency: d.currencyStore.currentCurrency + + accountAddress: ctrlAccount.currentValue ?? "" + showCommunityAssets: ctrlShowCommunityAssets.checked + searchString: ctrlSearch.text + } + } + + Component.onCompleted: d.rebuildFilter() + + Pane { + SplitView.fillWidth: true + SplitView.fillHeight: true + + background: Rectangle { + color: Theme.palette.baseColor3 + } + + Rectangle { + width: 380 + height: 200 + color: Theme.palette.statusListItem.backgroundColor + border.color: Theme.palette.primaryColor1 + border.width: 1 + anchors.centerIn: parent + + // tokensKey, name, symbol, decimals, currentCurrencyBalance (computed), marketDetails, balances -> [ chainId, address, balance, iconUrl ] + TokenSelectorView { + anchors.fill: parent + + model: d.adaptor.outputAssetsModel + + onTokenSelected: (tokensKey) => { + console.warn("!!! TOKEN SELECTED:", tokensKey) + logs.logEvent("TokenSelectorView::onTokenSelected", ["tokensKey"], arguments) + } + } + } + } + + LogsAndControlsPanel { + SplitView.minimumHeight: 400 + SplitView.preferredHeight: 400 + + logsView.logText: logs.logText + + RowLayout { + anchors.fill: parent + + ColumnLayout { + CheckBox { + id: ctrlTestNetworks + text: "Test networks enabled" + tristate: true + checkState: Qt.PartiallyChecked + onClicked: d.rebuildFilter() + } + + Repeater { + id: chainIdsRepeater + model: SortFilterProxyModel { + sourceModel: d.flatNetworks + filters: ValueFilter { + roleName: "isTest" + value: ctrlTestNetworks.checked + enabled: ctrlTestNetworks.checkState !== Qt.PartiallyChecked + } + } + delegate: CheckBox { + required property int chainId + required property string chainName + required property string shortName + required property bool isEnabled + checked: isEnabled + opacity: enabled ? 1 : 0.3 + text: "%1 (%2) - %3".arg(chainName).arg(shortName).arg(chainId) + onToggled: { + if (checked) + d.addFilter(chainId) + else + d.removeFilter(chainId) + } + } + } + + Label { + Layout.fillWidth: true + text: "Enabled chain ids: %1".arg(d.enabledChainIdsString) + } + } + + ColumnLayout { + RowLayout { + Layout.fillWidth: true + Label { text: "Search:" } + TextField { + Layout.fillWidth: true + id: ctrlSearch + placeholderText: "Token name or symbol" + } + } + Switch { + id: ctrlShowCommunityAssets + text: "Show community assets" + } + RowLayout { + Layout.fillWidth: true + Label { text: "Account:" } + ComboBox { + Layout.fillWidth: true + id: ctrlAccount + textRole: "name" + valueRole: "address" + displayText: currentIndex === -1 ? "All accounts" : currentText + model: SortFilterProxyModel { + sourceModel: d.walletAccountsModel + sorters: RoleSorter { roleName: "position" } + } + currentIndex: -1 + } + } + Label { + Layout.alignment: Qt.AlignRight + text: "Selected: %1".arg(ctrlAccount.currentValue ?? "all") + } + + Item { Layout.fillHeight: true } + } + } + } +} + +// category: Views diff --git a/storybook/qmlTests/tests/tst_TokenSelectorView.qml b/storybook/qmlTests/tests/tst_TokenSelectorView.qml new file mode 100644 index 000000000..cfa8e23fe --- /dev/null +++ b/storybook/qmlTests/tests/tst_TokenSelectorView.qml @@ -0,0 +1,82 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import Models 1.0 + +import AppLayouts.Wallet.views 1.0 +import AppLayouts.Wallet.stores 1.0 +import AppLayouts.Wallet.adaptors 1.0 + +Item { + id: root + width: 600 + height: 400 + + QtObject { + id: d + + 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 + } + + readonly property var adaptor: TokenSelectorViewAdaptor { + assetsModel: d.assetsStore.groupedAccountAssetsModel + flatNetworksModel: d.flatNetworks + currentCurrency: "USD" + } + } + + Component { + id: componentUnderTest + TokenSelectorView { + anchors.fill: parent + + model: d.adaptor.outputAssetsModel + } + } + + SignalSpy { + id: signalSpy + target: controlUnderTest + signalName: "tokenSelected" + } + + property TokenSelectorView controlUnderTest: null + + TestCase { + name: "TokenSelectorView" + when: windowShown + + function init() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + signalSpy.clear() + } + + function test_basicGeometry() { + verify(!!controlUnderTest) + verify(controlUnderTest.width > 0) + verify(controlUnderTest.height > 0) + } + + function test_clickEthToken() { + verify(!!controlUnderTest) + + const tokensKey = "ETH" + + const delegate = findChild(controlUnderTest, "tokenSelectorAssetDelegate_%1".arg(tokensKey)) + verify(!!delegate) + tryCompare(delegate, "tokensKey", tokensKey) + + // click the delegate, verify the signal has been fired and has the correct "tokensKey" as argument + mouseClick(delegate) + tryCompare(signalSpy, "count", 1) + compare(signalSpy.signalArguments[0][0], tokensKey) + } + } +} diff --git a/storybook/qmlTests/tests/tst_TokenSelectorViewAdaptor.qml b/storybook/qmlTests/tests/tst_TokenSelectorViewAdaptor.qml new file mode 100644 index 000000000..c947afcb8 --- /dev/null +++ b/storybook/qmlTests/tests/tst_TokenSelectorViewAdaptor.qml @@ -0,0 +1,111 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import Models 1.0 + +import StatusQ.Core.Utils 0.1 + +import AppLayouts.Wallet.stores 1.0 +import AppLayouts.Wallet.adaptors 1.0 + +Item { + id: root + width: 600 + height: 400 + + QtObject { + id: d + + 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 + } + } + + Component { + id: componentUnderTest + TokenSelectorViewAdaptor { + assetsModel: d.assetsStore.groupedAccountAssetsModel + flatNetworksModel: d.flatNetworks + currentCurrency: "USD" + } + } + + property TokenSelectorViewAdaptor controlUnderTest: null + + TestCase { + name: "TokenSelectorViewAdaptor" + when: windowShown + + function init() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + } + + function test_search() { + verify(!!controlUnderTest) + + const searchText = "dAi" + const originalCount = controlUnderTest.outputAssetsModel.count + controlUnderTest.searchString = searchText + + // search yields 1 result + tryCompare(controlUnderTest.outputAssetsModel, "count", 1) + + // resetting search string resets the view back to original count + controlUnderTest.searchString = "" + tryCompare(controlUnderTest.outputAssetsModel, "count", originalCount) + } + + function test_showCommunityAssets() { + verify(!!controlUnderTest) + + const originalCount = controlUnderTest.outputAssetsModel.count + + // turn on showing the community assets, verify we now have more items + controlUnderTest.showCommunityAssets = true + tryVerify(() => controlUnderTest.outputAssetsModel.count > originalCount) + + // turning them back off, verify we are back to the original number of items + controlUnderTest.showCommunityAssets = false + tryCompare(controlUnderTest.outputAssetsModel, "count", originalCount) + } + + function test_enabledChainIds() { + verify(!!controlUnderTest) + + // enable just "1" (Eth Mainnet) chain + controlUnderTest.enabledChainIds = [1] + + // grab the "DAI" entry + const delegate = ModelUtils.getByKey(controlUnderTest.outputAssetsModel, "tokensKey", "DAI") + verify(!!delegate) + const origBalance = delegate.currencyBalance + + // should have 0 balance + tryCompare(delegate, "currencyBalance", 0) + + // re-enable all chains, DAI should again have the original balance + controlUnderTest.enabledChainIds = [] + tryCompare(delegate, "currencyBalance", origBalance) + } + + function test_accountAddress() { + verify(!!controlUnderTest) + + // enable the "Hot wallet" account address filter (0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881) + controlUnderTest.accountAddress = "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881" + + // grab the "STT" entry + const delegate = ModelUtils.getByKey(controlUnderTest.outputAssetsModel, "tokensKey", "STT") + verify(!!delegate) + + // should have ~45.90 balance + fuzzyCompare(delegate.currencyBalance, 45.90, 0.01) + } + } +} diff --git a/storybook/src/Models/GroupedAccountsAssetsModel.qml b/storybook/src/Models/GroupedAccountsAssetsModel.qml index e1e000d8a..5747106a0 100644 --- a/storybook/src/Models/GroupedAccountsAssetsModel.qml +++ b/storybook/src/Models/GroupedAccountsAssetsModel.qml @@ -5,29 +5,33 @@ ListModel { { tokensKey: "DAI", balances: [ - { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 5, balance: "0" }, + { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 10, balance: "559133758939097000" }, + { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 420, balance: "0" }, { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 11155111, balance: "0" }, - { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 5, balance: "123456789123456789" }, - { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 11155111, balance: "123456789123456789" } + { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 420, balance: "123456789123456789" }, + { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 11155111, balance: "123456789123456789" }, + { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 42161, balance: "45123456789123456789" }, ] }, { tokensKey: "ETH", balances: [ + { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 1, balance: "122082928968121891" }, { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 420, balance: "1013151281976507736" }, { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 421613, balance: "473057568699284613" }, - { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 5, balance: "307400931315122839" }, + { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 420, balance: "307400931315122839" }, { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 11155111, balance: "307400931315122839" }, { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 420, balance: "122082928968121891" }, { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 421613, balance: "0" }, - { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 5, balance: "559133758939097000" } + { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 420, balance: "559133758939097000" } ] }, { tokensKey: "STT", balances: [ - { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 5, balance: "999999999998998500000000000016777216" }, - { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 5, balance: "1077000000000000000000" }, + { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 1, balance: "45123456789123456789" }, + { account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 420, balance: "999999999998998500000000000016777216" }, + { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 10, balance: "1077000000000000000000" }, { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 420, balance: "122082928968121891" }, { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 421613, balance: "222000000000000000" }, { account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 11155111, balance: "559133758939097000" } diff --git a/storybook/src/Models/NetworksModel.qml b/storybook/src/Models/NetworksModel.qml index 2d891bf37..65a7c85e4 100644 --- a/storybook/src/Models/NetworksModel.qml +++ b/storybook/src/Models/NetworksModel.qml @@ -56,7 +56,7 @@ QtObject { Component.onCompleted: append([ { chainId: 1, - chainName: "Ethereum Mainnet", + chainName: "Mainnet", blockExplorerUrl: "https://etherscan.io/", iconUrl: "network/Network=Ethereum", chainColor: "#627EEA", @@ -217,7 +217,6 @@ QtObject { chainName: "Arbitrum", iconUrl: ModelsData.networks.arbitrum, isActive: false, - isEnabled: true, shortName: "ARB", chainColor: "purple", layer: 2, @@ -337,7 +336,6 @@ QtObject { chainName: "Arbitrum", iconUrl: ModelsData.networks.arbitrum, isActive: false, - isEnabled: true, shortName: "ARB", chainColor: "purple", layer: 2, diff --git a/storybook/src/Models/TokensBySymbolModel.qml b/storybook/src/Models/TokensBySymbolModel.qml index 0c34e87b1..ae370952f 100644 --- a/storybook/src/Models/TokensBySymbolModel.qml +++ b/storybook/src/Models/TokensBySymbolModel.qml @@ -9,7 +9,6 @@ ListModel { readonly property string nativeSource: "native" //SourceOfTokensModel.custom readonly property var data: [ - { key: "ETH", name: "Ether", @@ -117,7 +116,7 @@ ListModel { changePctDay: 0, changePct24hour: 0, change24hour: 0, - currencyPrice: ({amount: 0, symbol: "USD", displayDecimals: 2, stripTrailingZeroes: false}) + currencyPrice: ({amount: 0.07, symbol: "USD", displayDecimals: 2, stripTrailingZeroes: false}) }, detailsLoading: false, marketDetailsLoading: false diff --git a/storybook/stubs/shared/stores/CurrenciesStore.qml b/storybook/stubs/shared/stores/CurrenciesStore.qml index 517dc1aac..a3a3b014a 100644 --- a/storybook/stubs/shared/stores/CurrenciesStore.qml +++ b/storybook/stubs/shared/stores/CurrenciesStore.qml @@ -17,10 +17,10 @@ QtObject { return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale) } - function formatCurrencyAmountFromBigInt(balance, symbol, decimals) { + function formatCurrencyAmountFromBigInt(balance, symbol, decimals, options = null) { let bigIntBalance = SQUtils.AmountsArithmetic.fromString(balance) let decimalBalance = SQUtils.AmountsArithmetic.toNumber(bigIntBalance, decimals) - return formatCurrencyAmount(decimalBalance, symbol) + return formatCurrencyAmount(decimalBalance, symbol, options) } function getFiatValue(balance, cryptoSymbol) { diff --git a/storybook/stubs/shared/stores/send/TransactionStore.qml b/storybook/stubs/shared/stores/send/TransactionStore.qml index 15ebff8e1..5d980f5c1 100644 --- a/storybook/stubs/shared/stores/send/TransactionStore.qml +++ b/storybook/stubs/shared/stores/send/TransactionStore.qml @@ -182,7 +182,7 @@ QtObject { let listOfChains = chainIds.split(":") let listOfChainIds = [] for (let k =0;k balanceThresholdAmount } - expectedRoles: ["isCommunityAsset", "currentCurrencyBalance"] + expectedRoles: ["currentCurrencyBalance"] enabled: balanceThresholdEnabled } ] diff --git a/ui/StatusQ/include/StatusQ/modelsyncedcontainer.h b/ui/StatusQ/include/StatusQ/modelsyncedcontainer.h index 7943654dd..498982b60 100644 --- a/ui/StatusQ/include/StatusQ/modelsyncedcontainer.h +++ b/ui/StatusQ/include/StatusQ/modelsyncedcontainer.h @@ -122,7 +122,7 @@ private: m_persistentIndexes.clear(); m_persistentIndexes.reserve(count); - for (decltype(count) i = 0; i < count; i++) + for (auto i = 0; i < count; i++) m_persistentIndexes.push_back(model->index(i, 0)); } diff --git a/ui/StatusQ/include/StatusQ/objectproxymodel.h b/ui/StatusQ/include/StatusQ/objectproxymodel.h index d3340a267..6e2924eb7 100644 --- a/ui/StatusQ/include/StatusQ/objectproxymodel.h +++ b/ui/StatusQ/include/StatusQ/objectproxymodel.h @@ -66,9 +66,8 @@ private: void updateRoleNames(); void updateIndexes(int from, int to); - QHash findExpectedRoles( - const QHash& roleNames, - const QStringList& expectedRoles); + QHash findExpectedRoles(const QHash &roleNames, + const QStringList &expectedRoles); QPointer m_delegate; QHash m_expectedRoleNames; diff --git a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml index bdcedf343..1b54429f1 100644 --- a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml +++ b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml @@ -73,19 +73,25 @@ QtObject { function stripTrailingZeroes(numStr, locale) { locale = locale || Qt.locale() - let regEx = locale.decimalPoint == "." ? /(\.[0-9]*[1-9])0+$|\.0*$/ : /(\,[0-9]*[1-9])0+$|\,0*$/ + let regEx = locale.decimalPoint === "." ? /(\.[0-9]*[1-9])0+$|\.0*$/ : /(\,[0-9]*[1-9])0+$|\,0*$/ return numStr.replace(regEx, '$1') } - function numberToLocaleString(num, precision = -1, locale = null) { + function numberToLocaleString(num, precision = -128 /* QLocale::FloatingPointShortest */, locale = null) { locale = locale || Qt.locale() - if (precision === -1) - precision = fractionalPartLength(num) - return num.toLocaleString(locale, 'f', precision) } + function currencyNumberToLocaleString(num, symbol = "", locale = null) { + locale = locale || Qt.locale() + + if (typeof num === "string") + num = Number(num) + + return num.toLocaleCurrencyString(locale, symbol) + } + function numberToLocaleStringInCompactForm(num, locale = null) { locale = locale || Qt.locale() const numberOfDigits = integralPartLength(num) diff --git a/ui/StatusQ/src/leftjoinmodel.cpp b/ui/StatusQ/src/leftjoinmodel.cpp index 361747806..d93ed7eca 100644 --- a/ui/StatusQ/src/leftjoinmodel.cpp +++ b/ui/StatusQ/src/leftjoinmodel.cpp @@ -23,7 +23,7 @@ void LeftJoinModel::initialize(bool reset) auto rightRoleNames = m_rightModel->roleNames(); auto leftNames = leftRoleNames.values(); - QList rightNames; + QByteArrayList rightNames; if (m_rolesToJoin.empty()) { rightNames = rightRoleNames.values(); @@ -41,7 +41,7 @@ void LeftJoinModel::initialize(bool reset) if (roles.empty()) { qWarning().noquote() - << QString("Role to join %1 not found in the right model!") + << QStringLiteral("Role to join %1 not found in the right model!") .arg(roleName); return; } @@ -264,11 +264,11 @@ QVariant LeftJoinModel::data(const QModelIndex& index, int role) const m_rightModel->index(0, 0), m_rightModelJoinRole, joinRoleLeftValue, 1, Qt::MatchExactly); - if (match.empty()) + if (match.isEmpty()) return {}; - m_lastUsedRightModelIndex = match.first(); - return match.first().data(role - m_rightModelRolesOffset); + m_lastUsedRightModelIndex = match.constFirst(); + return m_lastUsedRightModelIndex.data(role - m_rightModelRolesOffset); } void LeftJoinModel::classBegin() diff --git a/ui/StatusQ/src/objectproxymodel.cpp b/ui/StatusQ/src/objectproxymodel.cpp index dc7b3411e..3c04e0320 100644 --- a/ui/StatusQ/src/objectproxymodel.cpp +++ b/ui/StatusQ/src/objectproxymodel.cpp @@ -6,8 +6,6 @@ #include #include -#include - ObjectProxyModel::ObjectProxyModel(QObject* parent) : QIdentityProxyModel{parent} { @@ -184,8 +182,8 @@ QObject* ObjectProxyModel::proxyObject(int index) rowData->insert(i.value(), model->data(model->index(index, 0), i.key())); } - rowData->insert("index", index); - context->setContextProperty("model", rowData); + rowData->insert(QStringLiteral("index"), index); + context->setContextProperty(QStringLiteral("model"), rowData); QObject* instance = m_delegate->create(context); context->setParent(instance); @@ -300,7 +298,7 @@ void ObjectProxyModel::updateIndexes(int from, int to) auto& entry = m_container[i]; if (entry.proxy) - entry.rowData->insert("index", i); + entry.rowData->insert(QStringLiteral("index"), i); } } @@ -308,22 +306,24 @@ QHash ObjectProxyModel::findExpectedRoles( const QHash &roleNames, const QStringList &expectedRoles) { - if (roleNames.empty() || expectedRoles.isEmpty()) + if (roleNames.isEmpty() || expectedRoles.isEmpty()) return {}; QHash expected; - for (auto& role : expectedRoles) { + for (auto &role : expectedRoles) { auto expectedKeys = roleNames.keys(role.toUtf8()); auto expectedKeysCount = expectedKeys.size(); if (expectedKeysCount == 1) expected.insert(expectedKeys.first(), role.toUtf8()); else if (expectedKeysCount == 0) { - qWarning() << "Expected role not found!"; + qWarning() << Q_FUNC_INFO; + qWarning() << "Expected role" << role << "not found!"; } else { - qWarning() << "Malformed source model - multiple roles found for given " - "expected role name!"; + qWarning() << Q_FUNC_INFO; + qWarning() + << "Malformed source model - multiple roles found for given expected role name!"; return {}; } } diff --git a/ui/StatusQ/tests/tst_ObjectProxyModel.cpp b/ui/StatusQ/tests/tst_ObjectProxyModel.cpp index bad9cab35..d065d2a6e 100644 --- a/ui/StatusQ/tests/tst_ObjectProxyModel.cpp +++ b/ui/StatusQ/tests/tst_ObjectProxyModel.cpp @@ -263,9 +263,12 @@ private slots: model.setSourceModel(sourceModel); model.setDelegate(delegate.get()); - QTest::ignoreMessage(QtWarningMsg, "Expected role not found!"); + const auto expRoleName = QStringLiteral("undefined"); + QTest::ignoreMessage(QtWarningMsg, + QRegularExpression(QStringLiteral(".*findExpectedRoles*."))); + QTest::ignoreMessage(QtWarningMsg, QStringLiteral("Expected role \"%1\" not found!").arg(expRoleName).toLatin1()); - model.setExpectedRoles({ QStringLiteral("undefined") }); + model.setExpectedRoles({ expRoleName }); QCOMPARE(model.rowCount(), 3); } diff --git a/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml b/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml new file mode 100644 index 000000000..0c1e0dc2a --- /dev/null +++ b/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml @@ -0,0 +1,166 @@ +import QtQuick 2.15 + +import StatusQ 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 + +import SortFilterProxyModel 0.2 + +QObject { + id: root + + /** + Transforms and prepares input data (assets) for TokenSelectorView needs. The assets model is internally + joined with `flatNetworksModel` for the `balances` submodel + + Expected assets model structure: + - tokensKey: string -> 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 ] + + Computed values: + - currencyBalance: double (e.g. `1000.42` in user's fiat currency) + - currencyBalanceAsString: string (e.g. "1 000,42 CZK" formatted as a string according to the user's locale) + - balanceAsString: string (`1.42` formatted as e.g. "1,42" in user's locale) + */ + + // input API + required property var assetsModel + required property var flatNetworksModel + required property string currentCurrency // CurrenciesStore.currentCurrency, e.g. "USD" + + // optional filter properties; empty/default values means no filtering + property var enabledChainIds: [] + property string accountAddress + property bool showCommunityAssets + property string searchString + + // output model + readonly property SortFilterProxyModel outputAssetsModel: SortFilterProxyModel { + sourceModel: assetsObjectProxyModel + + filters: [ + AnyOf { + RegExpFilter { + roleName: "name" + pattern: root.searchString + caseSensitivity: Qt.CaseInsensitive + } + RegExpFilter { + roleName: "symbol" + pattern: root.searchString + caseSensitivity: Qt.CaseInsensitive + } + }, + ValueFilter { + roleName: "communityId" + value: "" + enabled: !root.showCommunityAssets + } + ] + + // FIXME optionally sort/filter by wallet controller as well + sorters: [ + RoleSorter { + roleName: "currencyBalance" + sortOrder: Qt.DescendingOrder + } + ] + } + + // internals + ObjectProxyModel { + id: assetsObjectProxyModel + sourceModel: root.assetsModel + + delegate: SortFilterProxyModel { + id: delegateRoot + + // properties exposed as roles to the top-level model + readonly property int decimals: model.decimals + readonly property double currentBalance: aggregator.value + readonly property double currencyBalance: { + if (!!model.marketDetails) { + return currentBalance * model.marketDetails.currencyPrice.amount + } + return 0 + } + readonly property int displayDecimals: !!model.marketDetails ? model.marketDetails.currencyPrice.displayDecimals : 0 + readonly property string currencyBalanceAsString: + currencyBalance ? LocaleUtils.currencyAmountToLocaleString({amount: currencyBalance, symbol: root.currentCurrency, displayDecimals}) + : "" + + readonly property var balances: this + + sourceModel: joinModel + + proxyRoles: [ + FastExpressionRole { + name: "balanceAsDouble" + function balanceToDouble(balance: string, decimals: int) { + if (typeof balance !== 'string') + return 0 + let bigIntBalance = AmountsArithmetic.fromString(balance) + return AmountsArithmetic.toNumber(bigIntBalance, decimals) + } + expression: balanceToDouble(model.balance, delegateRoot.decimals) + expectedRoles: ["balance"] + }, + FastExpressionRole { + name: "balanceAsString" + function convert(amount: double) { + return LocaleUtils.currencyAmountToLocaleString({amount, displayDecimals: 2}, {noSymbol: true}) + } + + expression: convert(model.balanceAsDouble) + expectedRoles: ["balanceAsDouble"] + } + ] + + filters: [ + ValueFilter { + roleName: "balance" + value: "0" + inverted: true + }, + FastExpressionFilter { + expression: root.enabledChainIds.includes(model.chainId) + expectedRoles: ["chainId"] + enabled: root.enabledChainIds.length + }, + RegExpFilter { + roleName: "account" + pattern: root.accountAddress + caseSensitivity: Qt.CaseInsensitive + enabled: root.accountAddress !== "" + } + ] + + sorters: [ + // sort by biggest (sub)balance first + RoleSorter { + roleName: "balanceAsDouble" + sortOrder: Qt.DescendingOrder + } + ] + + readonly property LeftJoinModel joinModel: LeftJoinModel { + leftModel: model.balances + rightModel: root.flatNetworksModel + joinRole: "chainId" + } + + readonly property SumAggregator aggregator: SumAggregator { + model: delegateRoot + roleName: "balanceAsDouble" + } + } + + exposedRoles: ["balances", "currencyBalance", "currencyBalanceAsString", "balanceAsString"] + expectedRoles: ["communityId", "balances", "decimals", "marketDetails"] + } +} diff --git a/ui/app/AppLayouts/Wallet/adaptors/qmldir b/ui/app/AppLayouts/Wallet/adaptors/qmldir new file mode 100644 index 000000000..088eb02a1 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/adaptors/qmldir @@ -0,0 +1 @@ +TokenSelectorViewAdaptor 1.0 TokenSelectorViewAdaptor.qml diff --git a/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml b/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml index e94d2d4f2..523fe6aa9 100644 --- a/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml +++ b/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml @@ -231,7 +231,7 @@ Control { return root.currencyStore.formatCurrencyAmount(balance, root.currencyStore.currentCurrency) } formatCurrencyAmountFromBigInt: function(balance, symbol, decimals) { - return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals) + return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, {noSymbol: true}) } onItemSelected: { d.setSelectedHoldingId(holdingId, holdingType) diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml index 1c071f770..e4aeb495c 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml @@ -22,6 +22,8 @@ QObject { property bool swapProposalReady: false property bool swapProposalLoading: false + property bool showCommunityTokens + // To expose the selected from and to Token from the SwapModal readonly property var fromToken: ModelUtils.getByKey(root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel, "key", root.swapFormData.fromTokensKey) readonly property var toToken: ModelUtils.getByKey(root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel, "key", root.swapFormData.toTokenKey) @@ -90,8 +92,8 @@ QObject { return root.currencyStore.formatCurrencyAmount(balance, symbol, options, locale) } - function formatCurrencyAmountFromBigInt(balance, symbol, decimals) { - return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals) + function formatCurrencyAmountFromBigInt(balance, symbol, decimals, options = null) { + return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, options) } function getAllChainIds() { @@ -122,14 +124,13 @@ QObject { property real displayAssetsBelowBalanceThresholdAmount: root.walletAssetsStore.walletTokensStore.getDisplayAssetsBelowBalanceThresholdDisplayAmount() sourceModel: __assetsWithFilteredBalances proxyRoles: [ - FastExpressionRole { - name: "isCommunityAsset" - expression: !!model.communityId - expectedRoles: ["communityId"] - }, FastExpressionRole { name: "currentBalance" - expression: __getTotalBalance(model.balances, model.decimals) + expression: { + // FIXME recalc when selectedNetworkChainId changes + root.swapFormData.selectedNetworkChainId + return __getTotalBalance(model.balances, model.decimals) + } expectedRoles: ["balances", "decimals"] }, FastExpressionRole { @@ -150,19 +151,16 @@ QObject { if (!root.walletAssetsStore.assetsController.filterAcceptsSymbol(model.symbol)) // explicitely hidden return false - if (model.isCommunityAsset) // do not show community assets - return false + if (!!model.communityId) + return root.showCommunityTokens if (root.walletAssetsStore.walletTokensStore.displayAssetsBelowBalance) return model.currentCurrencyBalance > processedAssetsModel.displayAssetsBelowBalanceThresholdAmount return true } - expectedRoles: ["symbol", "isCommunityAsset", "currentCurrencyBalance"] + expectedRoles: ["symbol", "communityId", "currentCurrencyBalance"] } ] // FIXME sort by assetsController instead, to have the sorting/order as in the main wallet view - // sorters: RoleSorter { - // roleName: "isCommunityAsset" - // } } // Internal properties and functions ----------------------------------------------------------------------------------------------------------------------------- @@ -203,7 +201,7 @@ QObject { } readonly property LeftJoinModel joinModel: LeftJoinModel { leftModel: submodel - rightModel: root.filteredFlatNetworksModel + rightModel: root.swapStore.flatNetworks joinRole: "chainId" } @@ -227,11 +225,12 @@ QObject { } /* Internal function to calculate total balance */ - function __getTotalBalance(balances, decimals) { + function __getTotalBalance(balances, decimals, chainIds = [root.swapFormData.selectedNetworkChainId]) { let totalBalance = 0 for(let i=0; i model.balances submodel [chainId: int, balance: BigIntString] + flatNetworks [account:string, iconUrl: string] + required property var balancesModel + + property bool interactive: true + + signal assetSelected(string tokensKey) + + spacing: Style.current.halfPadding + horizontalPadding: Style.current.padding + verticalPadding: 4 + + opacity: interactive ? 1 : 0.3 + + implicitWidth: ListView.view.width + implicitHeight: 60 + + icon.width: 32 + icon.height: 32 + icon.source: Constants.tokenIcon(symbol) + + enabled: interactive + + background: Rectangle { + radius: Style.current.radius + color: (root.interactive && root.hovered) || root.highlighted ? Theme.palette.statusListItem.highlightColor + : "transparent" + HoverHandler { + cursorShape: root.interactive ? Qt.PointingHandCursor : undefined + } + } + + contentItem: RowLayout { + spacing: root.spacing + + // asset icon + StatusRoundedImage { + Layout.preferredWidth: root.icon.width + Layout.preferredHeight: root.icon.height + image.source: root.icon.source + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + // name, symbol, total balance + RowLayout { + Layout.fillWidth: true + spacing: root.spacing + Item { + id: nameRow + Layout.fillWidth: true + Layout.preferredHeight: childrenRect.height + StatusBaseText { + id: nameText + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: Math.min(implicitWidth, nameRow.width - symbolText.width - symbolText.anchors.leftMargin) + text: root.name + font.weight: Font.Medium + elide: Text.ElideRight + } + StatusBaseText { + id: symbolText + anchors.left: nameText.right + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + text: root.symbol + color: Theme.palette.baseColor1 + } + } + StatusBaseText { + font.weight: Font.Medium + text: root.currencyBalanceAsString + } + } + + // balances per network chain + StatusListView { + Layout.maximumWidth: parent.width + Layout.preferredWidth: contentWidth + Layout.preferredHeight: 22 + orientation: ListView.Horizontal + spacing: root.spacing + visible: count + interactive: !root.ListView.view.moving + ScrollBar.horizontal: null + + model: root.balancesModel + delegate: RowLayout { + height: ListView.view.height + spacing: 4 + StatusRoundedImage { + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + image.source: Style.svg("tiny/%1".arg(model.iconUrl)) + } + StatusBaseText { + font.pixelSize: Theme.tertiaryTextFontSize + text: model.balanceAsString + } + } + // let the root handle the click + TapHandler { + onTapped: root.clicked() + } + } + } + } + + onClicked: root.assetSelected(root.tokensKey) +} diff --git a/ui/app/AppLayouts/Wallet/views/TokenSelectorView.qml b/ui/app/AppLayouts/Wallet/views/TokenSelectorView.qml new file mode 100644 index 000000000..a23686fa8 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/views/TokenSelectorView.qml @@ -0,0 +1,28 @@ +import QtQuick 2.15 + +import StatusQ.Core 0.1 + +StatusListView { + id: root + + // expected model structure: + // tokensKey, name, symbol, decimals, currencyBalanceAsString (computed), marketDetails, balances -> [ chainId, address, balance, iconUrl ] + + // output API + signal tokenSelected(string tokensKey) + + currentIndex: -1 + + delegate: TokenSelectorAssetDelegate { + required property var model + required property int index + + tokensKey: model.tokensKey + name: model.name + symbol: model.symbol + currencyBalanceAsString: model.currencyBalanceAsString + balancesModel: model.balances + + onAssetSelected: (tokensKey) => root.tokenSelected(tokensKey) + } +} diff --git a/ui/app/AppLayouts/Wallet/views/qmldir b/ui/app/AppLayouts/Wallet/views/qmldir index ee26025ae..f779a633f 100644 --- a/ui/app/AppLayouts/Wallet/views/qmldir +++ b/ui/app/AppLayouts/Wallet/views/qmldir @@ -1,3 +1,5 @@ -CollectiblesView 1.0 CollectiblesView.qml AssetsDetailView 1.0 AssetsDetailView.qml +CollectiblesView 1.0 CollectiblesView.qml SavedAddresses 1.0 SavedAddresses.qml +TokenSelectorAssetDelegate 1.0 TokenSelectorAssetDelegate.qml +TokenSelectorView 1.0 TokenSelectorView.qml diff --git a/ui/imports/shared/popups/send/SendModal.qml b/ui/imports/shared/popups/send/SendModal.qml index 09a23ac31..687961f7f 100644 --- a/ui/imports/shared/popups/send/SendModal.qml +++ b/ui/imports/shared/popups/send/SendModal.qml @@ -267,7 +267,7 @@ StatusDialog { return popup.store.currencyStore.formatCurrencyAmount(balance, popup.store.currencyStore.currentCurrency) } formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){ - return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals) + return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals, {noSymbol: true}) } } @@ -391,7 +391,7 @@ StatusDialog { return popup.store.currencyStore.formatCurrencyAmount(balance, popup.store.currencyStore.currentCurrency) } formatCurrencyAmountFromBigInt: function(balance, symbol, decimals) { - return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals) + return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals, {noSymbol: true}) } } diff --git a/ui/imports/shared/stores/CurrenciesStore.qml b/ui/imports/shared/stores/CurrenciesStore.qml index 55bde4df3..4f452f6f1 100644 --- a/ui/imports/shared/stores/CurrenciesStore.qml +++ b/ui/imports/shared/stores/CurrenciesStore.qml @@ -14,21 +14,13 @@ QtObject { property var _profileSectionModuleInst: profileSectionModule function getModelIndexForKey(key) { - for (var i=0; i