diff --git a/storybook/pages/SwapInputPanelPage.qml b/storybook/pages/SwapInputPanelPage.qml index 1797cef08c..51320ffc41 100644 --- a/storybook/pages/SwapInputPanelPage.qml +++ b/storybook/pages/SwapInputPanelPage.qml @@ -35,6 +35,7 @@ SplitView { fromTokenAmount: ctrlFromTokenAmount.text toTokenKey: ctrlToTokenKey.text toTokenAmount: ctrlToTokenAmount.text + selectedAccountAddress: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" } readonly property SwapModalAdaptor adaptor: SwapModalAdaptor { diff --git a/storybook/pages/SwapModalPage.qml b/storybook/pages/SwapModalPage.qml index edc6b6fe8e..e32150fcd4 100644 --- a/storybook/pages/SwapModalPage.qml +++ b/storybook/pages/SwapModalPage.qml @@ -63,64 +63,79 @@ SplitView { Component.onCompleted: d.launchPopup() - SwapInputParamsForm { - id: swapInputForm - selectedAccountIndex: accountComboBox.currentIndex - selectedNetworkChainId: d.getNetwork() - fromTokensKey: fromTokenComboBox.currentValue - fromTokenAmount: swapInput.text - toTokenKey: toTokenComboBox.currentValue - toTokenAmount: swapOutputAmount.text + SwapStore { + id: dSwapStore + signal suggestedRoutesReady(var txRoutes) + readonly property var accounts: d.accountsModel + readonly property var flatNetworks: d.flatNetworksModel + readonly property bool areTestNetworksEnabled: areTestNetworksEnabledCheckbox.checked + + function fetchSuggestedRoutes(accountFrom, accountTo, amount, tokenFrom, tokenTo, + disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts) { + console.debug("fetchSuggestedRoutes called >> accountFrom = ",accountFrom, " accountTo =", + accountTo, "amount = ",amount, " tokenFrom = ",tokenFrom, " tokenTo = ", tokenTo, + " disabledFromChainIDs = ",disabledFromChainIDs, " disabledToChainIDs = ",disabledToChainIDs, + " preferredChainIDs = ",preferredChainIDs, " sendType =", sendType, " lockedInAmounts = ",lockedInAmounts) + } + function authenticateAndTransfer(uuid, accountFrom, accountTo, tokenFrom, + tokenTo, sendType, tokenName, tokenIsOwnerToken, paths) { + console.debug("authenticateAndTransfer called >> uuid ", uuid, " accountFrom = ",accountFrom, " accountTo =", + accountTo, "tokenFrom = ",tokenFrom, " tokenTo = ",tokenTo, " sendType = ", sendType, + " tokenName = ", tokenName, " tokenIsOwnerToken = ", tokenIsOwnerToken, " paths = ", paths) + } + function getWei2Eth(wei, decimals) { + return wei/(10**decimals) + } } - SwapModalAdaptor { - id: swapModalAdaptor - swapStore: SwapStore { - signal suggestedRoutesReady(var txRoutes) - readonly property var accounts: d.accountsModel - readonly property var flatNetworks: d.flatNetworksModel - readonly property bool areTestNetworksEnabled: areTestNetworksEnabledCheckbox.checked - - function fetchSuggestedRoutes(accountFrom, accountTo, amount, tokenFrom, tokenTo, - disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts) { - console.debug("fetchSuggestedRoutes called >> accountFrom = ",accountFrom, " accountTo =", - accountTo, "amount = ",amount, " tokenFrom = ",tokenFrom, " tokenTo = ", tokenTo, - " disabledFromChainIDs = ",disabledFromChainIDs, " disabledToChainIDs = ",disabledToChainIDs, - " preferredChainIDs = ",preferredChainIDs, " sendType =", sendType, " lockedInAmounts = ",lockedInAmounts) - } - function authenticateAndTransfer(uuid, accountFrom, accountTo, tokenFrom, - tokenTo, sendType, tokenName, tokenIsOwnerToken, paths) { - console.debug("authenticateAndTransfer called >> uuid ", uuid, " accountFrom = ",accountFrom, " accountTo =", - accountTo, "tokenFrom = ",tokenFrom, " tokenTo = ",tokenTo, " sendType = ", sendType, - " tokenName = ", tokenName, " tokenIsOwnerToken = ", tokenIsOwnerToken, " paths = ", paths) - } - function getWei2Eth(wei, decimals) { - return wei/(10**decimals) - } - } - walletAssetsStore: WalletAssetsStore { - id: thisWalletAssetStore - walletTokensStore: TokensStore { - readonly property var plainTokensBySymbolModel: TokensBySymbolModel {} - getDisplayAssetsBelowBalanceThresholdDisplayAmount: () => 0 - } - readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} - assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel - } - currencyStore: CurrenciesStore {} - swapFormData: swapInputForm - swapOutputData: SwapOutputData{} + TokensStore { + id: tokensStore + readonly property var plainTokensBySymbolModel: TokensBySymbolModel {} + getDisplayAssetsBelowBalanceThresholdDisplayAmount: () => 0 } Component { id: swapModal SwapModal { + id: modal visible: true modal: false closePolicy: Popup.CloseOnEscape destroyOnClose: true - swapInputParamsForm: swapInputForm - swapAdaptor: swapModalAdaptor + swapInputParamsForm: SwapInputParamsForm { + selectedAccountAddress: { + if (accountComboBox.model.count > 0 && accountComboBox.currentIndex >= 0) { + return ModelUtils.get(accountComboBox.model, accountComboBox.currentIndex, "address") + } + return "" + } + selectedNetworkChainId: d.getNetwork() + fromTokensKey: fromTokenComboBox.currentValue + fromTokenAmount: swapInput.text + toTokenKey: toTokenComboBox.currentValue + } + swapAdaptor: SwapModalAdaptor { + swapStore: dSwapStore + walletAssetsStore: WalletAssetsStore { + id: thisWalletAssetStore + walletTokensStore: tokensStore + readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} + assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel + } + currencyStore: CurrenciesStore {} + swapFormData: modal.swapInputParamsForm + swapOutputData: SwapOutputData{} + } + Binding { + target: swapInputParamsForm + property: "fromTokensKey" + value: fromTokenComboBox.currentValue + } + Binding { + target: swapInputParamsForm + property: "toTokenKey" + value: toTokenComboBox.currentValue + } } } } @@ -157,9 +172,6 @@ SplitView { sorters: RoleSorter { roleName: "position"; sortOrder: Qt.AscendingOrder } } currentIndex: 0 - onCurrentIndexChanged: { - swapInputForm.selectedAccountIndex = currentIndex - } } StatusBaseText { @@ -171,7 +183,6 @@ SplitView { model: d.filteredNetworksModel currentIndex: 0 onCountChanged: currentIndex = 0 - onCurrentIndexChanged: swapInputForm.selectedNetworkChainId = d.getNetwork() } StatusBaseText { @@ -182,14 +193,13 @@ SplitView { textRole: "name" valueRole: "key" model: d.tokenBySymbolModel - currentIndex: 0 } StatusInput { id: swapInput Layout.preferredWidth: 100 label: "Token amount to swap" - text: "100" + text: "" } StatusBaseText { @@ -206,21 +216,21 @@ SplitView { Button { text: "emit no routes found event" onClicked: { - swapModalAdaptor.swapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txNoRoutes) + dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txNoRoutes) } } Button { text: "emit no approval needed route" onClicked: { - swapModalAdaptor.swapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRouteNoApproval) + dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRouteNoApproval) } } Button { text: "emit approval needed route" onClicked: { - swapModalAdaptor.swapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded) + dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded) } } } diff --git a/storybook/qmlTests/tests/tst_SwapInputPanel.qml b/storybook/qmlTests/tests/tst_SwapInputPanel.qml index 2eeeac213e..4bfbf630ae 100644 --- a/storybook/qmlTests/tests/tst_SwapInputPanel.qml +++ b/storybook/qmlTests/tests/tst_SwapInputPanel.qml @@ -2,6 +2,7 @@ import QtQuick 2.15 import QtTest 1.15 import StatusQ 0.1 +import StatusQ.Core.Utils 0.1 import AppLayouts.Wallet.stores 1.0 import AppLayouts.Wallet.panels 1.0 @@ -37,7 +38,9 @@ Item { assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel } currencyStore: CurrenciesStore {} - swapFormData: SwapInputParamsForm {} + swapFormData: SwapInputParamsForm { + selectedAccountAddress: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" + } swapOutputData: SwapOutputData {} } } @@ -135,7 +138,7 @@ Item { const amountToSendInput = findChild(controlUnderTest, "amountToSendInput") verify(!!amountToSendInput) - tryCompare(amountToSendInput.input, "text", Number(tokenAmount).toLocaleString(Qt.locale(), 'f', -128)) + tryCompare(amountToSendInput.input, "text", AmountsArithmetic.fromString(tokenAmount).toLocaleString(Qt.locale(), 'f', -128)) } function test_enterTokenAmountLocalizedNumber() { diff --git a/storybook/qmlTests/tests/tst_SwapModal.qml b/storybook/qmlTests/tests/tst_SwapModal.qml index 1b8659f3f5..224cf48636 100644 --- a/storybook/qmlTests/tests/tst_SwapModal.qml +++ b/storybook/qmlTests/tests/tst_SwapModal.qml @@ -108,16 +108,31 @@ Item { return accountsModalHeader } - function verifyLoadingAndNoErrorsState() { + function verifyLoadingAndNoErrorsState(payPanel, receivePanel) { // verify loading state was set and no errors currently verify(!root.swapAdaptor.validSwapProposalReceived) verify(root.swapAdaptor.swapProposalLoading) - compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "0") - compare(root.swapAdaptor.swapOutputData.toTokenAmount, "0") + compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "") + compare(root.swapAdaptor.swapOutputData.toTokenAmount, "") compare(root.swapAdaptor.swapOutputData.totalFees, 0) compare(root.swapAdaptor.swapOutputData.bestRoutes, []) compare(root.swapAdaptor.swapOutputData.approvalNeeded, false) compare(root.swapAdaptor.swapOutputData.hasError, false) + + // verfy input and output panels + verify(!payPanel.loading) + compare(payPanel.selectedHoldingId, root.swapFormData.fromTokensKey) + compare(payPanel.cryptoValue, Number(root.swapFormData.fromTokenAmount)) + compare(payPanel.cryptoValueRaw, SQUtils.AmountsArithmetic.fromNumber(root.swapFormData.fromTokenAmount, root.swapAdaptor.fromToken.decimals).toString()) + verify(payPanel.cryptoValueValid) + verify(receivePanel.loading) + verify(!receivePanel.interactive) + compare(receivePanel.selectedHoldingId, root.swapFormData.toTokenKey) + /* TODO: there is bug which prevents us from testing this right now + The value is not updated after setting tokenAmount to empty string in the receive input panel + https://github.com/status-im/status-desktop/issues/15162 + compare(receivePanel.cryptoValue, 0) + compare(receivePanel.cryptoValueRaw, "0") */ } // end helper functions ------------------------------------------------------------- @@ -126,7 +141,7 @@ Item { /* 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++) { - root.swapFormData.selectedAccountIndex = i + root.swapFormData.selectedAccountAddress = swapAdaptor.nonWatchAccounts.get(i).address // Launch popup launchAndVerfyModal() @@ -291,7 +306,7 @@ Item { verify(accountsModalHeader.control.popup.closed) // The input params form's slected Index should be updated as per this selection - compare(root.swapFormData.selectedAccountIndex, i) + compare(root.swapFormData.selectedAccountAddress, swapAdaptor.nonWatchAccounts.get(i).address) // The comboBox item should reflect chosen account const floatingHeaderBackground = findChild(accountsModalHeader, "headerBackground") @@ -417,8 +432,6 @@ Item { } function test_edit_slippage() { - // by default the max slippage button should show no values and the edit button shouldnt be visible - // Launch popup launchAndVerfyModal() @@ -476,8 +489,6 @@ Item { } function test_modal_swap_proposal_setup() { - // by default the max slippage button should show no values and the edit button shouldnt be visible - root.swapAdaptor.reset() // Launch popup @@ -495,6 +506,12 @@ Item { const errorTag = findChild(controlUnderTest, "errorTag") verify(!!errorTag) + const payPanel = findChild(controlUnderTest, "payPanel") + verify(!!payPanel) + + const receivePanel = findChild(controlUnderTest, "receivePanel") + verify(!!receivePanel) + // Check max fees values and sign button state when nothing is set compare(maxFeesText.text, qsTr("Max fees:")) compare(maxFeesValue.text, "--") @@ -506,16 +523,18 @@ Item { compare(formValuesChanged.count, 1) root.swapFormData.toTokenKey = root.swapAdaptor.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel.get(1).key compare(formValuesChanged.count, 2) - root.swapFormData.fromTokenAmount = 10 + root.swapFormData.fromTokenAmount = "0.001" compare(formValuesChanged.count, 3) root.swapFormData.selectedNetworkChainId = root.swapAdaptor.filteredFlatNetworksModel.get(0).chainId compare(formValuesChanged.count, 4) + root.swapFormData.selectedAccountAddress = root.swapAdaptor.nonWatchAccounts.get(0).address + compare(formValuesChanged.count, 5) // wait for fetchSuggestedRoutes function to be called wait(1000) // verify loading state was set and no errors currently - verifyLoadingAndNoErrorsState() + verifyLoadingAndNoErrorsState(payPanel, receivePanel) // emit event that no routes were found root.swapStore.suggestedRoutesReady(root.dummySwapTransactionRoutes.txNoRoutes) @@ -523,8 +542,8 @@ Item { // verify loading state was removed and that error was displayed verify(!root.swapAdaptor.validSwapProposalReceived) verify(!root.swapAdaptor.swapProposalLoading) - compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "0") - compare(root.swapAdaptor.swapOutputData.toTokenAmount, "0") + compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "") + compare(root.swapAdaptor.swapOutputData.toTokenAmount, "") compare(root.swapAdaptor.swapOutputData.totalFees, 0) compare(root.swapAdaptor.swapOutputData.bestRoutes, []) compare(root.swapAdaptor.swapOutputData.approvalNeeded, false) @@ -534,24 +553,32 @@ Item { verify(!signButton.enabled) compare(signButton.text, qsTr("Swap")) + // verfy input and output panels + verify(!payPanel.loading) + verify(!receivePanel.loading) + verify(!receivePanel.interactive) + compare(receivePanel.selectedHoldingId, root.swapFormData.toTokenKey) + compare(receivePanel.cryptoValue, 0) + compare(receivePanel.cryptoValueRaw, "0") + // edit some params to retry swap - root.swapFormData.fromTokenAmount = 11 - compare(formValuesChanged.count, 5) + root.swapFormData.fromTokenAmount = "0.00011" + compare(formValuesChanged.count, 6) // wait for fetchSuggestedRoutes function to be called wait(1000) // verify loading state was set and no errors currently - verifyLoadingAndNoErrorsState() + verifyLoadingAndNoErrorsState(payPanel, receivePanel) // emit event with route that needs no approval let txRoutes = root.dummySwapTransactionRoutes.txHasRouteNoApproval root.swapStore.suggestedRoutesReady(txRoutes) - // verify loading state removed and data ius displayed as expected on the Modal + // verify loading state removed and data is displayed as expected on the Modal verify(root.swapAdaptor.validSwapProposalReceived) verify(!root.swapAdaptor.swapProposalLoading) - compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "0") + compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "") compare(root.swapAdaptor.swapOutputData.toTokenAmount, root.swapStore.getWei2Eth(txRoutes.amountToReceive, root.swapAdaptor.toToken.decimals).toString()) // calculation needed for total fees @@ -567,15 +594,24 @@ Item { verify(signButton.enabled) compare(signButton.text, qsTr("Swap")) + // verfy input and output panels + waitForRendering(receivePanel) + verify(payPanel.cryptoValueValid) + verify(!receivePanel.loading) + verify(!receivePanel.interactive) + compare(receivePanel.selectedHoldingId, root.swapFormData.toTokenKey) + compare(receivePanel.cryptoValue, root.swapStore.getWei2Eth(txRoutes.amountToReceive, root.swapAdaptor.toToken.decimals)) + compare(receivePanel.cryptoValueRaw, SQUtils.AmountsArithmetic.fromNumber(root.swapAdaptor.swapOutputData.toTokenAmount, root.swapAdaptor.toToken.decimals).toString()) + // edit some params to retry swap - root.swapFormData.fromTokenAmount = 1 - compare(formValuesChanged.count, 6) + root.swapFormData.fromTokenAmount = "0.012" + compare(formValuesChanged.count, 7) // wait for fetchSuggestedRoutes function to be called wait(1000) // verify loading state was set and no errors currently - verifyLoadingAndNoErrorsState() + verifyLoadingAndNoErrorsState(payPanel, receivePanel) // emit event with route that needs no approval let txRoutes2 = root.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded @@ -584,7 +620,7 @@ Item { // verify loading state removed and data ius displayed as expected on the Modal verify(root.swapAdaptor.validSwapProposalReceived) verify(!root.swapAdaptor.swapProposalLoading) - compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "0") + compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "") compare(root.swapAdaptor.swapOutputData.toTokenAmount, root.swapStore.getWei2Eth(txRoutes2.amountToReceive, root.swapAdaptor.toToken.decimals).toString()) // calculation needed for total fees @@ -599,6 +635,497 @@ Item { verify(!errorTag.visible) verify(signButton.enabled) compare(signButton.text, qsTr("Approve %1").arg(root.swapAdaptor.fromToken.symbol)) + + // verfy input and output panels + waitForRendering(receivePanel) + verify(payPanel.cryptoValueValid) + verify(!receivePanel.loading) + verify(!receivePanel.interactive) + compare(receivePanel.selectedHoldingId, root.swapFormData.toTokenKey) + compare(receivePanel.cryptoValue, root.swapStore.getWei2Eth(txRoutes.amountToReceive, root.swapAdaptor.toToken.decimals)) + compare(receivePanel.cryptoValueRaw, SQUtils.AmountsArithmetic.fromNumber(root.swapAdaptor.swapOutputData.toTokenAmount, root.swapAdaptor.toToken.decimals).toString()) + } + + function test_modal_pay_input_default() { + // Launch popup + launchAndVerfyModal() + + const payPanel = findChild(controlUnderTest, "payPanel") + verify(!!payPanel) + const amountToSendInput = findChild(payPanel, "amountToSendInput") + verify(!!amountToSendInput) + const bottomItemText = findChild(payPanel, "bottomItemText") + verify(!!bottomItemText) + const holdingSelector = findChild(payPanel, "holdingSelector") + verify(!!holdingSelector) + const maxTagButton = findChild(payPanel, "maxTagButton") + verify(!!maxTagButton) + const holdingSelectorsContentItemText = findChild(payPanel, "holdingSelectorsContentItemText") + verify(!!holdingSelectorsContentItemText) + const holdingSelectorsTokenIcon = findChild(payPanel, "holdingSelectorsTokenIcon") + verify(!!holdingSelectorsTokenIcon) + + waitForRendering(payPanel) + + // check default states for the from input selector + compare(amountToSendInput.caption, qsTr("Pay")) + verify(amountToSendInput.interactive) + compare(amountToSendInput.input.text, "") + verify(amountToSendInput.input.input.edit.cursorVisible) + compare(amountToSendInput.input.placeholderText, LocaleUtils.numberToLocaleString(0)) + compare(bottomItemText.text, root.swapAdaptor.currencyStore.formatCurrencyAmount(0, root.swapAdaptor.currencyStore.currentCurrency)) + compare(holdingSelector.selectedItem, undefined) + compare(holdingSelectorsContentItemText.text, qsTr("Select asset")) + compare(holdingSelectorsTokenIcon.image.source, "") + verify(!holdingSelectorsTokenIcon.visible) + verify(!maxTagButton.visible) + compare(payPanel.selectedHoldingId, "") + compare(payPanel.cryptoValue, 0) + compare(payPanel.cryptoValueRaw, "0") + verify(!payPanel.cryptoValueValid) + + closeAndVerfyModal() + } + + function test_modal_pay_input_presetValues() { + // 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 + root.swapFormData.selectedNetworkChainId = root.swapAdaptor.filteredFlatNetworksModel.get(0).chainId + root.swapFormData.fromTokensKey = "ETH" + root.swapFormData.fromTokenAmount = valueToExchangeString + + let expectedToken = SQUtils.ModelUtils.getByKey(root.swapAdaptor.processedAssetsModel, "tokensKey", "ETH") + + // Launch popup + launchAndVerfyModal() + + const payPanel = findChild(controlUnderTest, "payPanel") + verify(!!payPanel) + const amountToSendInput = findChild(payPanel, "amountToSendInput") + verify(!!amountToSendInput) + const bottomItemText = findChild(payPanel, "bottomItemText") + verify(!!bottomItemText) + const holdingSelector = findChild(payPanel, "holdingSelector") + verify(!!holdingSelector) + const maxTagButton = findChild(payPanel, "maxTagButton") + verify(!!maxTagButton) + const holdingSelectorsContentItemText = findChild(payPanel, "holdingSelectorsContentItemText") + verify(!!holdingSelectorsContentItemText) + const holdingSelectorsTokenIcon = findChild(payPanel, "holdingSelectorsTokenIcon") + verify(!!holdingSelectorsTokenIcon) + + waitForRendering(payPanel) + + compare(amountToSendInput.caption, qsTr("Pay")) + verify(amountToSendInput.interactive) + compare(amountToSendInput.input.text, valueToExchangeString) + compare(amountToSendInput.input.placeholderText, LocaleUtils.numberToLocaleString(0)) + verify(amountToSendInput.input.input.edit.cursorVisible) + compare(bottomItemText.text, root.swapAdaptor.currencyStore.formatCurrencyAmount(valueToExchange * expectedToken.marketDetails.currencyPrice.amount, root.swapAdaptor.currencyStore.currentCurrency)) + compare(holdingSelector.selectedItem, expectedToken) + compare(holdingSelectorsContentItemText.text, expectedToken.symbol) + compare(holdingSelectorsTokenIcon.image.source, Constants.tokenIcon(expectedToken.symbol)) + verify(holdingSelectorsTokenIcon.visible) + verify(maxTagButton.visible) + compare(maxTagButton.text, qsTr("Max. %1").arg(root.swapAdaptor.currencyStore.formatCurrencyAmount(Math.trunc(WalletUtils.calculateMaxSafeSendAmount(expectedToken.currentBalance, expectedToken.symbol)*100)/100, expectedToken.symbol, {noSymbol: true}))) + compare(payPanel.selectedHoldingId, expectedToken.symbol) + compare(payPanel.cryptoValue, valueToExchange) + compare(payPanel.cryptoValueRaw, SQUtils.AmountsArithmetic.fromNumber(valueToExchangeString, expectedToken.decimals).toString()) + verify(payPanel.cryptoValueValid) + + closeAndVerfyModal() + } + + function test_modal_pay_input_wrong_value_1() { + let invalidValues = ["ABC", "0.0.010201", "12PASA", "100,9.01"] + for (let i =0; i maxPossibleValue ? qsTr("Insufficient funds for swap"): */qsTr("An error has occured, please try again") + let buttonText = /*valueToExchange > maxPossibleValue ? qsTr("Buy crypto"):*/ "" + compare(errorTag.visible, false/*valueToExchange > maxPossibleValue*/) + compare(errorTag.text, errortext) + compare(errorTag.buttonText, buttonText) + } + + closeAndVerfyModal() + } } } } diff --git a/ui/app/AppLayouts/Wallet/WalletLayout.qml b/ui/app/AppLayouts/Wallet/WalletLayout.qml index f0d1d81d28..f44d75531c 100644 --- a/ui/app/AppLayouts/Wallet/WalletLayout.qml +++ b/ui/app/AppLayouts/Wallet/WalletLayout.qml @@ -136,7 +136,7 @@ Item { } property SwapInputParamsForm swapFormData: SwapInputParamsForm { - selectedAccountIndex: d.selectedAccountIndex + selectedAccountAddress: StatusQUtils.ModelUtils.get(RootStore.nonWatchAccounts, d.selectedAccountIndex, "address") selectedNetworkChainId: { // Without this when we switch testnet mode, the correct network is not evaluated RootStore.areTestNetworksEnabled @@ -216,7 +216,7 @@ Item { hasFloatingButtons: true }) onLaunchSwapModal: { - d.swapFormData.selectedAccountIndex = d.selectedAccountIndex + d.swapFormData.selectedAccountAddress = StatusQUtils.ModelUtils.get(RootStore.nonWatchAccounts, d.selectedAccountIndex, "address") d.swapFormData.selectedNetworkChainId = StatusQUtils.ModelUtils.getByKey(RootStore.filteredFlatModel, "layer", 1, "chainId") d.swapFormData.fromTokensKey = tokensKey d.swapFormData.toTokenKey = RootStore.areTestNetworksEnabled ? Constants.swap.testStatusTokenKey : Constants.swap.mainnetStatusTokenKey @@ -335,7 +335,7 @@ Item { } onLaunchSwapModal: { d.swapFormData.fromTokensKey = "" - d.swapFormData.selectedAccountIndex = d.selectedAccountIndex + d.swapFormData.selectedAccountAddress = StatusQUtils.ModelUtils.get(RootStore.nonWatchAccounts, d.selectedAccountIndex, "address") d.swapFormData.selectedNetworkChainId = StatusQUtils.ModelUtils.getByKey(RootStore.filteredFlatModel, "layer", 1, "chainId") if(!!walletStore.currentViewedHoldingTokensKey && walletStore.currentViewedHoldingType === Constants.TokenType.ERC20) { d.swapFormData.fromTokensKey = walletStore.currentViewedHoldingTokensKey diff --git a/ui/app/AppLayouts/Wallet/WalletUtils.qml b/ui/app/AppLayouts/Wallet/WalletUtils.qml index b83af36972..d2658d8bf4 100644 --- a/ui/app/AppLayouts/Wallet/WalletUtils.qml +++ b/ui/app/AppLayouts/Wallet/WalletUtils.qml @@ -67,7 +67,7 @@ QtObject { rationale: https://github.com/status-im/status-desktop/pull/14959#discussion_r1627110880 */ function calculateMaxSafeSendAmount(value, symbol) { - if (symbol !== Constants.ethToken) { + if (symbol !== Constants.ethToken || value === 0) { return value } diff --git a/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml b/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml index 523fe6aa92..bd1c79c70c 100644 --- a/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml +++ b/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml @@ -29,25 +29,31 @@ Control { required property var processedAssetsModel property string tokenKey - onTokenKeyChanged: { - if (!!tokenKey) - Qt.callLater(d.setSelectedHoldingId, tokenKey, Constants.TokenType.ERC20) - } + onTokenKeyChanged: reevaluateSelectedId() property string tokenAmount onTokenAmountChanged: { if (!!tokenAmount) - Qt.callLater(() => amountToSendInput.input.text = Number(tokenAmount).toLocaleString(Qt.locale(), 'f', -128)) + Qt.callLater(() => amountToSendInput.input.text = SQUtils.AmountsArithmetic.fromString(tokenAmount).toLocaleString(locale, 'f', -128)) } property int swapSide: SwapInputPanel.SwapSide.Pay property bool fiatInputInteractive property bool loading + property bool interactive: true // output API readonly property string selectedHoldingId: d.selectedHoldingId readonly property double cryptoValue: amountToSendInput.cryptoValueToSendFloat readonly property string cryptoValueRaw: amountToSendInput.cryptoValueToSend readonly property bool cryptoValueValid: amountToSendInput.inputNumberValid + /* TODO: this does not work as expected because of bug - + https://github.com/status-im/status-desktop/issues/15162 */ + readonly property bool amountEnteredGreaterThanBalance: cryptoValue > maxSendButton.maxSafeValue + function reevaluateSelectedId() { + if (!!tokenKey) { + Qt.callLater(d.setSelectedHoldingId, tokenKey, Constants.TokenType.ERC20) + } + } // visual properties property int swapExchangeButtonWidth: 44 @@ -185,7 +191,7 @@ Control { id: amountToSendInput objectName: "amountToSendInput" caption: root.caption - interactive: true + interactive: root.interactive selectedHolding: d.selectedHolding fiatInputInteractive: root.fiatInputInteractive input.input.edit.color: !input.valid ? Theme.palette.dangerColor1 : maxSendButton.hovered ? Theme.palette.baseColor1 diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapInputParamsForm.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapInputParamsForm.qml index 420220b8c8..2528868662 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapInputParamsForm.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapInputParamsForm.qml @@ -9,15 +9,15 @@ QtObject { signal formValuesChanged() - property int selectedAccountIndex: 0 + property string selectedAccountAddress: "" property int selectedNetworkChainId: -1 property string fromTokensKey: "" - property string fromTokenAmount: "0" + property string fromTokenAmount: "" property string toTokenKey: "" - property string toTokenAmount: "0" + property string toTokenAmount: "" property double selectedSlippage: 0.5 - onSelectedAccountIndexChanged: root.formValuesChanged() + onSelectedAccountAddressChanged: root.formValuesChanged() onSelectedNetworkChainIdChanged: root.formValuesChanged() onFromTokensKeyChanged: root.formValuesChanged() onFromTokenAmountChanged: root.formValuesChanged() @@ -25,17 +25,17 @@ QtObject { onToTokenAmountChanged: root.formValuesChanged() function resetFormData() { - selectedAccountIndex = 0 + selectedAccountAddress = "" selectedNetworkChainId = -1 fromTokensKey = "" - fromTokenAmount = "0" + fromTokenAmount = "" toTokenKey = "" - toTokenAmount = "0" + toTokenAmount = "" selectedSlippage = 0.5 } function isFormFilledCorrectly() { - return root.selectedAccountIndex >= 0 && + return !!root.selectedAccountAddress && root.selectedNetworkChainId !== -1 && !!root.fromTokensKey && !!root.toTokenKey && ((!!root.fromTokenAmount && diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml index 41b4c736ba..72b4e43e88 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml @@ -15,6 +15,7 @@ import shared.popups.send.controls 1.0 import shared.controls 1.0 import AppLayouts.Wallet.controls 1.0 +import AppLayouts.Wallet.panels 1.0 StatusDialog { id: root @@ -35,17 +36,29 @@ StatusDialog { QtObject { id: d - property var fetchSuggestedRoutes: Backpressure.debounce(root, 1000, function() { - root.swapAdaptor.fetchSuggestedRoutes() + property var debounceFetchSuggestedRoutes: Backpressure.debounce(root, 1000, function() { + root.swapAdaptor.fetchSuggestedRoutes(payPanel.cryptoValueRaw) }) + + function fetchSuggestedRoutes() { + root.swapAdaptor.newFetchReset() + root.swapAdaptor.swapProposalLoading = true + debounceFetchSuggestedRoutes() + } } Connections { target: root.swapInputParamsForm function onFormValuesChanged() { - root.swapAdaptor.swapProposalLoading = true d.fetchSuggestedRoutes() } + // refresh the selected asset in payPanel when account/network changes + function onSelectedAccountAddressChanged() { + payPanel.reevaluateSelectedId() + } + function onSelectedNetworkChainIdChanged() { + payPanel.reevaluateSelectedId() + } } Behavior on implicitHeight { @@ -63,9 +76,9 @@ StatusDialog { formatCurrencyAmount: root.swapAdaptor.formatCurrencyAmount /* TODO: once the Account Header is reworked we simply should be able to use an index and not this logic of selectedAccount being set */ - selectedAccount: root.swapAdaptor.getSelectedAccount(root.swapInputParamsForm.selectedAccountIndex) + selectedAccount: root.swapAdaptor.getSelectedAccountByAddress(root.swapInputParamsForm.selectedAccountAddress) onSelectedIndexChanged: { - root.swapInputParamsForm.selectedAccountIndex = selectedIndex + root.swapInputParamsForm.selectedAccountAddress = root.swapAdaptor.getSelectedAccountAddressByIndex(selectedIndex) } } @@ -115,60 +128,88 @@ StatusDialog { } } - // This is a temporary placeholder while each of the components are being added. - ColumnLayout { + Item { Layout.fillWidth: true - spacing: 0 - StatusBaseText { - text: "This area is a temporary placeholder" - font.bold: true - } - /* TODO: Will be swapped out under https://github.com/status-im/status-desktop/issues/14825 - Will also handle that if one input is being edited the other one should be in loading state in that task */ - StatusBaseText { - text: qsTr("Selected from token: %1").arg(swapInputParamsForm.fromTokensKey) - } - StatusInput { - id: fromTokenAmountInput - Layout.fillWidth: true - text: swapInputParamsForm.fromTokenAmount - onTextChanged: { - swapInputParamsForm.fromTokenAmount = text + Layout.topMargin: 2 + Layout.preferredHeight: payPanel.height + receivePanel.height + 4 + + SwapInputPanel { + id: payPanel + objectName: "payPanel" + + anchors { + left: parent.left + right: parent.right + top: parent.top } - } - /* TODO: Will be swapped out under https://github.com/status-im/status-desktop/issues/14826 - Will also handle that if one input is being edited the other one should be in loading state in that task */ - StatusBaseText { - text: qsTr("Selected to token: %1").arg(swapInputParamsForm.toTokenKey) - } - StatusInput { - id: toTokenAmountInput - Layout.fillWidth: true - text: root.swapAdaptor.validSwapProposalReceived && root.swapAdaptor.toToken ? - root.swapAdaptor.formatCurrencyAmount(root.swapAdaptor.swapOutputData.toTokenAmount, - root.swapAdaptor.toToken.symbol, - {"minDecimals": root.swapAdaptor.toToken.decimals, - "stripTrailingZeroes": true, "noSymbol": true}) : - root.swapInputParamsForm.toTokenAmount - onTextChanged: { - if (!root.swapAdaptor.validSwapProposalReceived) { - swapInputParamsForm.toTokenAmount = text + + currencyStore: root.swapAdaptor.currencyStore + flatNetworksModel: root.swapAdaptor.filteredFlatNetworksModel + processedAssetsModel: root.swapAdaptor.processedAssetsModel + + tokenKey: root.swapInputParamsForm.fromTokensKey + tokenAmount: { + // Only update if there is different in amount displayed + if (root.swapInputParamsForm.fromTokenAmount !== + SQUtils.AmountsArithmetic.fromString(cryptoValue).toLocaleString(locale, 'f', -128)){ + return root.swapInputParamsForm.fromTokenAmount } + return payPanel.tokenAmount } - /* TODO: keep this input as disabled until the work for adding a param to handle to - and from tokens inputed is supported by backend */ - input.edit.enabled: false + + swapSide: SwapInputPanel.SwapSide.Pay + swapExchangeButtonWidth: swapButton.width + + onSelectedHoldingIdChanged: root.swapInputParamsForm.fromTokensKey = selectedHoldingId + onCryptoValueChanged: root.swapInputParamsForm.fromTokenAmount = cryptoValue.toLocaleString(locale, 'f', -128) } - /* Needed only till sign after approval is implemented under - https://github.com/status-im/status-desktop/issues/14833 */ - StatusButton { - text: "Final Swap after Approval" - onClicked: { - swapAdaptor.sendSwapTx() + + SwapInputPanel { + id: receivePanel + objectName: "receivePanel" + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom } + + currencyStore: root.swapAdaptor.currencyStore + flatNetworksModel: root.swapAdaptorfilteredFlatNetworksModel + processedAssetsModel: root.swapAdaptor.processedAssetsModel + + tokenKey: root.swapInputParamsForm.toTokenKey + tokenAmount: root.swapAdaptor.validSwapProposalReceived && root.swapAdaptor.toToken ? root.swapAdaptor.swapOutputData.toTokenAmount: root.swapInputParamsForm.toTokenAmount + + swapSide: SwapInputPanel.SwapSide.Receive + swapExchangeButtonWidth: swapButton.width + + loading: root.swapAdaptor.swapProposalLoading + + onSelectedHoldingIdChanged: root.swapInputParamsForm.toTokenKey = selectedHoldingId + + /* TODO: keep this input as disabled until the work for adding a param to handle to + and from tokens inputed is supported by backend under + https://github.com/status-im/status-desktop/issues/15095 */ + interactive: false + } + + SwapExchangeButton { + id: swapButton + anchors.centerIn: parent + } + } + + /* TODO: remove! Needed only till sign after approval is implemented under + https://github.com/status-im/status-desktop/issues/14833 */ + StatusButton { + text: "Final Swap after Approval" + visible: root.swapAdaptor.validSwapProposalReceived && root.swapAdaptor.swapOutputData.approvalNeeded + onClicked: { + swapAdaptor.sendSwapTx() + close() } } - // End temporary placeholders EditSlippagePanel { id: editSlippagePanel @@ -186,10 +227,17 @@ StatusDialog { ErrorTag { objectName: "errorTag" - visible: root.swapAdaptor.swapOutputData.hasError + visible: root.swapAdaptor.swapOutputData.hasError || payPanel.amountEnteredGreaterThanBalance Layout.alignment: Qt.AlignHCenter Layout.topMargin: Style.current.smallPadding - text: qsTr("An error has occured, please try again") + text: { + if (payPanel.amountEnteredGreaterThanBalance) { + return qsTr("Insufficient funds for swap") + } + return qsTr("An error has occured, please try again") + } + buttonText: payPanel.amountEnteredGreaterThanBalance ? qsTr("Buy crypto"): "" + onButtonClicked: Global.openBuyCryptoModalRequested() } } @@ -265,7 +313,9 @@ StatusDialog { qsTr("Approve %1").arg(!!root.swapAdaptor.fromToken ? root.swapAdaptor.fromToken.symbol: "") : qsTr("Swap") disabledColor: Theme.palette.directColor8 - enabled: root.swapAdaptor.validSwapProposalReceived && editSlippagePanel.valid + enabled: root.swapAdaptor.validSwapProposalReceived && + editSlippagePanel.valid && + !payPanel.amountEnteredGreaterThanBalance onClicked: { if (root.swapAdaptor.validSwapProposalReceived ){ if(root.swapAdaptor.swapOutputData.approvalNeeded) { @@ -273,8 +323,8 @@ StatusDialog { } else { swapAdaptor.sendSwapTx() + close() } - close() } } } diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml index adf66c4f47..07d6e17df9 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml @@ -115,12 +115,11 @@ QObject { roleName: "chainId" value: root.swapFormData.selectedNetworkChainId enabled: root.swapFormData.selectedNetworkChainId !== -1 - }/*, - // TODO enable once AccountsModalHeader is reworked!! + }, ValueFilter { roleName: "account" - value: root.selectedSenderAccount.address - }*/ + value: root.swapFormData.selectedAccountAddress + } ] } } @@ -203,6 +202,13 @@ QObject { root.swapProposalLoading = false } + // this function will not reset input params but only the output ones and loading states + function newFetchReset() { + root.swapOutputData.reset() + root.validSwapProposalReceived = false + root.swapProposalLoading = false + } + function getNetworkShortNames(chainIds) { var networkString = "" let chainIdsArray = chainIds.split(":") @@ -239,34 +245,35 @@ QObject { } // TODO: remove once the AccountsModalHeader is reworked!! - function getSelectedAccount(index) { + function getSelectedAccountAddressByIndex(index) { if (root.nonWatchAccounts.count > 0 && index >= 0) { - return ModelUtils.get(nonWatchAccounts, index) + return ModelUtils.get(nonWatchAccounts, index, "address") + } + return "" + } + + function getSelectedAccountByAddress(address) { + if (root.nonWatchAccounts.count > 0 && !!address) { + return ModelUtils.getByKey(root.nonWatchAccounts, "address", address) } return null } - function fetchSuggestedRoutes() { - let amount = !!root.swapFormData.fromTokenAmount ? AmountsArithmetic.fromString(root.swapFormData.fromTokenAmount): NaN - root.swapOutputData.reset() - - if(!isNaN(amount) && !!root.fromToken && root.swapFormData.isFormFilledCorrectly()) { - let fromTokenAmountInWei = AmountsArithmetic.fromNumber(amount, !!root.fromToken ? root.fromToken.decimals: 18).toString() - + function fetchSuggestedRoutes(cryptoValueRaw) { + if (root.swapFormData.isFormFilledCorrectly() && !!cryptoValueRaw) { + root.swapOutputData.reset() root.validSwapProposalReceived = false // Identify new swap with a different uuid d.uuid = Utils.uuid() - let account = getSelectedAccount(root.swapFormData.selectedAccountIndex) + let account = getSelectedAccountByAddress(root.swapFormData.selectedAccountAddress) let accountAddress = account.address let disabledChainIds = getDisabledChainIds(root.swapFormData.selectedNetworkChainId) let preferedChainIds = getAllChainIds() - // TODO #14825: amount should be in BigInt string representation (fromTokenAmount * 10^decimals) - // Make sure that's replaced when the input component is integrated root.swapStore.fetchSuggestedRoutes(accountAddress, accountAddress, - fromTokenAmountInWei, root.swapFormData.fromTokensKey, root.swapFormData.toTokenKey, + cryptoValueRaw, root.swapFormData.fromTokensKey, root.swapFormData.toTokenKey, disabledChainIds, disabledChainIds, preferedChainIds, Constants.SendType.Swap, "") } else { @@ -276,7 +283,7 @@ QObject { } function sendApproveTx() { - let account = getSelectedAccount(root.swapFormData.selectedAccountIndex) + let account = getSelectedAccountByAddress(root.swapFormData.selectedAccountAddress) let accountAddress = account.address root.swapStore.authenticateAndTransfer(d.uuid, accountAddress, accountAddress, @@ -285,7 +292,7 @@ QObject { } function sendSwapTx() { - let account = getSelectedAccount(root.swapFormData.selectedAccountIndex) + let account = getSelectedAccountByAddress(root.swapFormData.selectedAccountAddress) let accountAddress = account.address root.swapStore.authenticateAndTransfer(d.uuid, accountAddress, accountAddress, diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapOutputData.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapOutputData.qml index afb0911179..b5e8c562ff 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapOutputData.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapOutputData.qml @@ -5,8 +5,8 @@ to the swap request can be placed here at one place. */ QtObject { id: root - property string fromTokenAmount: "0" - property string toTokenAmount: "0" + property string fromTokenAmount: "" + property string toTokenAmount: "" property real totalFees: 0 property var bestRoutes: [] property bool approvalNeeded @@ -14,8 +14,8 @@ QtObject { property var rawPaths: [] function reset() { - root.fromTokenAmount = "0" - root.toTokenAmount = "0" + root.fromTokenAmount = "" + root.toTokenAmount = "" root.totalFees = 0 root.bestRoutes = [] root.approvalNeeded = false diff --git a/ui/imports/shared/popups/send/panels/HoldingItemSelector.qml b/ui/imports/shared/popups/send/panels/HoldingItemSelector.qml index 9aac3b44dd..530871b0c9 100644 --- a/ui/imports/shared/popups/send/panels/HoldingItemSelector.qml +++ b/ui/imports/shared/popups/send/panels/HoldingItemSelector.qml @@ -91,6 +91,7 @@ Item { contentItem: RowLayout { StatusRoundedImage { id: tokenIcon + objectName: "holdingSelectorsTokenIcon" Layout.preferredWidth: root.contentIconSize Layout.preferredHeight: root.contentIconSize visible: !!d.iconSource @@ -102,6 +103,7 @@ Item { } } StatusBaseText { + objectName: "holdingSelectorsContentItemText" Layout.fillWidth: true font.pixelSize: root.contentTextSize elide: Text.ElideRight diff --git a/ui/imports/shared/popups/send/views/AmountToSend.qml b/ui/imports/shared/popups/send/views/AmountToSend.qml index 2d5fd8d1cf..9e92ad7e12 100644 --- a/ui/imports/shared/popups/send/views/AmountToSend.qml +++ b/ui/imports/shared/popups/send/views/AmountToSend.qml @@ -95,8 +95,11 @@ ColumnLayout { d.cryptoValueToSend, root.multiplierIndex).toString() } - readonly property string zeroString: - LocaleUtils.numberToLocaleString(0, 2, topAmountToSendInput.locale) + // Crypto value should be represented by 0 and fiat with 0.00 + readonly property string zeroString: { + let decimals = root.inputIsFiat ? 2 : 0 + LocaleUtils.numberToLocaleString(0, decimals, topAmountToSendInput.locale) + } readonly property double parsedInput: LocaleUtils.numberFromLocaleString(topAmountToSendInput.text,