From 83db905e28839f806c5e8b50c268cd9d27aabbe9 Mon Sep 17 00:00:00 2001 From: Dario Gabriel Lipicar Date: Fri, 19 Jul 2024 00:36:36 -0300 Subject: [PATCH] feat(swap): added more explicit error messages Closes #14956 --- .../main/wallet_section/send/controller.nim | 2 +- .../main/wallet_section/send/io_interface.nim | 2 +- .../main/wallet_section/send/module.nim | 4 +- .../modules/main/wallet_section/send/view.nim | 10 +- .../service/transaction/service.nim | 15 +- storybook/pages/SendModalPage.qml | 4 +- storybook/pages/SwapModalPage.qml | 34 ++-- storybook/qmlTests/tests/tst_SwapModal.qml | 162 +++++++++++++++++- .../shared/stores/send/TransactionStore.qml | 2 +- .../Wallet/popups/swap/SwapModal.qml | 21 ++- .../Wallet/popups/swap/SwapModalAdaptor.qml | 58 ++++++- .../Wallet/popups/swap/SwapOutputData.qml | 4 + ui/app/AppLayouts/Wallet/stores/SwapStore.qml | 6 +- ui/imports/shared/popups/send/SendModal.qml | 2 +- ui/imports/utils/Constants.qml | 7 + vendor/status-go | 2 +- 16 files changed, 283 insertions(+), 52 deletions(-) diff --git a/src/app/modules/main/wallet_section/send/controller.nim b/src/app/modules/main/wallet_section/send/controller.nim index 2a66ee27bc..f7714f1358 100644 --- a/src/app/modules/main/wallet_section/send/controller.nim +++ b/src/app/modules/main/wallet_section/send/controller.nim @@ -70,7 +70,7 @@ proc init*(self: Controller) = self.events.on(SIGNAL_SUGGESTED_ROUTES_READY) do(e:Args): let args = SuggestedRoutesArgs(e) - self.delegate.suggestedRoutesReady(args.uuid, args.suggestedRoutes) + self.delegate.suggestedRoutesReady(args.uuid, args.suggestedRoutes, args.errCode, args.errDescription) self.events.on(SignalType.WalletSignTransactions.event) do(e:Args): var data = WalletSignal(e) diff --git a/src/app/modules/main/wallet_section/send/io_interface.nim b/src/app/modules/main/wallet_section/send/io_interface.nim index 4b37332d0f..f6e57b0bc4 100644 --- a/src/app/modules/main/wallet_section/send/io_interface.nim +++ b/src/app/modules/main/wallet_section/send/io_interface.nim @@ -37,7 +37,7 @@ method suggestedRoutes*(self: AccessInterface, extraParamsTable: Table[string, string] = initTable[string, string]()) {.base.} = raise newException(ValueError, "No implementation available") -method suggestedRoutesReady*(self: AccessInterface, uuid: string, suggestedRoutes: SuggestedRoutesDto) {.base.} = +method suggestedRoutesReady*(self: AccessInterface, uuid: string, suggestedRoutes: SuggestedRoutesDto, errCode: string, errDescription: string) {.base.} = raise newException(ValueError, "No implementation available") method authenticateAndTransfer*(self: AccessInterface, from_addr: string, to_addr: string, assetKey: string, diff --git a/src/app/modules/main/wallet_section/send/module.nim b/src/app/modules/main/wallet_section/send/module.nim index 8397272a28..1d73483100 100644 --- a/src/app/modules/main/wallet_section/send/module.nim +++ b/src/app/modules/main/wallet_section/send/module.nim @@ -276,7 +276,7 @@ method transactionWasSent*(self: Module, chainId: int, txHash, uuid, error: stri return self.view.sendTransactionSentSignal(chainId, txHash, uuid, error) -method suggestedRoutesReady*(self: Module, uuid: string, suggestedRoutes: SuggestedRoutesDto) = +method suggestedRoutesReady*(self: Module, uuid: string, suggestedRoutes: SuggestedRoutesDto, errCode: string, errDescription: string) = self.tmpSendTransactionDetails.paths = suggestedRoutes.best self.tmpSendTransactionDetails.slippagePercentage = none(float) let paths = suggestedRoutes.best.map(x => self.convertTransactionPathDtoToSuggestedRouteItem(x)) @@ -294,7 +294,7 @@ method suggestedRoutesReady*(self: Module, uuid: string, suggestedRoutes: Sugges amountToReceive = suggestedRoutes.amountToReceive, toNetworksRouteModel = toNetworksRouteModel, rawPaths = suggestedRoutes.rawBest) - self.view.setTransactionRoute(transactionRoutes) + self.view.setTransactionRoute(transactionRoutes, errCode, errDescription) method suggestedRoutes*(self: Module, uuid: string, diff --git a/src/app/modules/main/wallet_section/send/view.nim b/src/app/modules/main/wallet_section/send/view.nim index aa5b788090..7e9bf72799 100644 --- a/src/app/modules/main/wallet_section/send/view.nim +++ b/src/app/modules/main/wallet_section/send/view.nim @@ -18,6 +18,8 @@ QtObject: fromNetworksRouteModel: NetworkRouteModel toNetworksRouteModel: NetworkRouteModel transactionRoutes: TransactionRoutes + errCode: string + errDescription: string selectedAssetKey: string selectedToAssetKey: string showUnPreferredChains: bool @@ -194,10 +196,12 @@ QtObject: self.delegate.authenticateAndTransfer(self.selectedSenderAccountAddress, self.selectedRecipient, self.selectedAssetKey, self.selectedToAssetKey, uuid, self.sendType, self.selectedTokenName, self.selectedTokenIsOwnerToken) - proc suggestedRoutesReady*(self: View, suggestedRoutes: QVariant) {.signal.} - proc setTransactionRoute*(self: View, routes: TransactionRoutes) = + proc suggestedRoutesReady*(self: View, suggestedRoutes: QVariant, errCode: string, errDescription: string) {.signal.} + proc setTransactionRoute*(self: View, routes: TransactionRoutes, errCode: string, errDescription: string) = self.transactionRoutes = routes - self.suggestedRoutesReady(newQVariant(self.transactionRoutes)) + self.errCode = errCode + self.errDescription = errDescription + self.suggestedRoutesReady(newQVariant(self.transactionRoutes), errCode, errDescription) proc suggestedRoutes*(self: View, amountIn: string, amountOut: string, extraParamsJson: string) {.slot.} = var extraParamsTable: Table[string, string] diff --git a/src/app_service/service/transaction/service.nim b/src/app_service/service/transaction/service.nim index bd75b140f3..936417d680 100644 --- a/src/app_service/service/transaction/service.nim +++ b/src/app_service/service/transaction/service.nim @@ -113,6 +113,8 @@ type SuggestedRoutesArgs* = ref object of Args uuid*: string suggestedRoutes*: SuggestedRoutesDto + errCode*: string + errDescription*: string type CryptoServicesArgs* = ref object of Args @@ -131,7 +133,7 @@ QtObject: tokenService: token_service.Service ## Forward declarations - proc suggestedRoutesV2Ready(self: Service, uuid: string, route: seq[TransactionPathDtoV2], routeRaw: string, error: string, errCode: string) + proc suggestedRoutesV2Ready(self: Service, uuid: string, route: seq[TransactionPathDtoV2], routeRaw: string, errCode: string, errDescription: string) proc delete*(self: Service) = self.QObject.delete @@ -162,7 +164,7 @@ QtObject: self.events.on(SignalType.WalletSuggestedRoutes.event) do(e:Args): var data = WalletSignal(e) - self.suggestedRoutesV2Ready(data.uuid, data.bestRoute, data.bestRouteRaw, data.error, data.errorCode) + self.suggestedRoutesV2Ready(data.uuid, data.bestRoute, data.bestRouteRaw, data.errorCode, data.error) self.events.on(PendingTransactionTypeDto.WalletTransfer.event) do(e: Args): try: @@ -578,7 +580,7 @@ QtObject: except Exception as e: error "Error getting suggested fees", msg = e.msg - proc suggestedRoutesV2Ready(self: Service, uuid: string, route: seq[TransactionPathDtoV2], routeRaw: string, error: string, errCode: string) = + proc suggestedRoutesV2Ready(self: Service, uuid: string, route: seq[TransactionPathDtoV2], routeRaw: string, errCode: string, errDescription: string) = # TODO: refactor sending modal part of the app, but for now since we're integrating the router v2 just map params to the old dto var oldRoute = convertToOldRoute(route) @@ -590,7 +592,12 @@ QtObject: amountToReceive: getTotalAmountToReceive(oldRoute), toNetworks: getToNetworksList(oldRoute), ) - self.events.emit(SIGNAL_SUGGESTED_ROUTES_READY, SuggestedRoutesArgs(uuid: uuid, suggestedRoutes: suggestedDto)) + self.events.emit(SIGNAL_SUGGESTED_ROUTES_READY, SuggestedRoutesArgs( + uuid: uuid, + suggestedRoutes: suggestedDto, + errCode: errCode, + errDescription: errDescription + )) proc suggestedRoutes*(self: Service, uuid: string, diff --git a/storybook/pages/SendModalPage.qml b/storybook/pages/SendModalPage.qml index adff334139..202241c08e 100644 --- a/storybook/pages/SendModalPage.qml +++ b/storybook/pages/SendModalPage.qml @@ -241,9 +241,11 @@ SplitView { amountToReceive: txStore.amountToSend - (txStore.amountToSend*5/100), toNetworksRouteModel: dummyEventData.toModel } + let errCode = "" + let errDescription = "" txStore.fromNetworksRouteModel.updateFromNetworks(dummyEventData.suggestesRoutes) txStore.toNetworksRouteModel.updateToNetworks(dummyEventData.suggestesRoutes) - txStore.walletSectionSendInst.suggestedRoutesReady(txRoutes) + txStore.walletSectionSendInst.suggestedRoutesReady(txRoutes, errCode, errDescription) txStore.suggestedRoutesCalled = false } } diff --git a/storybook/pages/SwapModalPage.qml b/storybook/pages/SwapModalPage.qml index 8a9def6bdb..7e646cd38a 100644 --- a/storybook/pages/SwapModalPage.qml +++ b/storybook/pages/SwapModalPage.qml @@ -72,7 +72,7 @@ SplitView { SwapStore { id: dSwapStore - signal suggestedRoutesReady(var txRoutes) + signal suggestedRoutesReady(var txRoutes, string errCode, string errDescription) signal transactionSent(var chainId, var txHash, var uuid, var error) signal transactionSendingComplete(var txHash, var success) @@ -278,7 +278,7 @@ SplitView { swapInput.text = "0.2" fetchSuggestedRoutesSpy.wait() Backpressure.debounce(this, 250, () => { - dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRouteNoApproval) + dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRouteNoApproval, "", "") })() } } @@ -292,7 +292,7 @@ SplitView { swapInput.text = "0.1" fetchSuggestedRoutesSpy.wait() Backpressure.debounce(this, 1000, () => { - dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded) + dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded, "", "") })() Backpressure.debounce(this, 1500, () => {approveTxButton.clicked()})() authenticateAndTransferSpy.wait() @@ -304,7 +304,7 @@ SplitView { })() fetchSuggestedRoutesSpy.wait() Backpressure.debounce(this, 1000, () => { - dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRouteNoApproval) + dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRouteNoApproval, "", "") })() } } @@ -317,7 +317,7 @@ SplitView { swapInput.text = "0.2" fetchSuggestedRoutesSpy.wait() Backpressure.debounce(this, 250, () => { - dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txNoRoutes) + dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txNoRoutes, "ERR-123", "Fetching proposal error") })() } @@ -332,7 +332,7 @@ SplitView { swapInput.text = "0.1" fetchSuggestedRoutesSpy.wait() Backpressure.debounce(this, 1000, () => { - dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded) + dSwapStore.suggestedRoutesReady(d.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded, "", "") })() Backpressure.debounce(this, 1500, () => {approveTxButton.clicked()})() authenticateAndTransferSpy.wait() @@ -351,12 +351,26 @@ SplitView { checked: false } + ComboBox { + id: routerErrorComboBox + model: [ + {name: "errNotEnoughTokenBalance", value: Constants.swap.errorCodes.errNotEnoughTokenBalance}, + {name: "errNotEnoughNativeBalance", value: Constants.swap.errorCodes.errNotEnoughNativeBalance}, + {name: "errPriceTimeout", value: Constants.swap.errorCodes.errPriceTimeout}, + {name: "errNotEnoughLiquidity", value: Constants.swap.errorCodes.errNotEnoughLiquidity} + ] + textRole: "name" + valueRole: "value" + currentIndex: 0 + visible: advancedSignalsCheckBox.checked + } + Button { - text: "emit no routes found event" + text: "emit no routes found event with error" onClicked: { const txRoutes = d.dummySwapTransactionRoutes.txNoRoutes txRoutes.uuid = d.uuid - dSwapStore.suggestedRoutesReady(txRoutes) + dSwapStore.suggestedRoutesReady(txRoutes, routerErrorComboBox.currentValue, "") } visible: advancedSignalsCheckBox.checked } @@ -366,7 +380,7 @@ SplitView { onClicked: { const txRoutes = d.dummySwapTransactionRoutes.txHasRouteNoApproval txRoutes.uuid = d.uuid - dSwapStore.suggestedRoutesReady(txRoutes) + dSwapStore.suggestedRoutesReady(txRoutes, "", "") } visible: advancedSignalsCheckBox.checked } @@ -376,7 +390,7 @@ SplitView { onClicked: { const txRoutes = d.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded txRoutes.uuid = d.uuid - dSwapStore.suggestedRoutesReady(txRoutes) + dSwapStore.suggestedRoutesReady(txRoutes, "", "") } visible: advancedSignalsCheckBox.checked } diff --git a/storybook/qmlTests/tests/tst_SwapModal.qml b/storybook/qmlTests/tests/tst_SwapModal.qml index 9b2e24e672..07001357c3 100644 --- a/storybook/qmlTests/tests/tst_SwapModal.qml +++ b/storybook/qmlTests/tests/tst_SwapModal.qml @@ -27,7 +27,7 @@ Item { readonly property var dummySwapTransactionRoutes: SwapTransactionRoutes {} readonly property var swapStore: SwapStore { - signal suggestedRoutesReady(var txRoutes) + signal suggestedRoutesReady(var txRoutes, string errCode, string errDescription) signal transactionSent(var chainId,var txHash, var uuid, var error) signal transactionSendingComplete(var txHash, var success) @@ -596,8 +596,8 @@ Item { // verify loading state was set and no errors currently verifyLoadingAndNoErrorsState(payPanel, receivePanel) - // emit event that no routes were found - root.swapStore.suggestedRoutesReady(root.dummySwapTransactionRoutes.txNoRoutes) + // emit event that no routes were found with unknown error + root.swapStore.suggestedRoutesReady(root.dummySwapTransactionRoutes.txNoRoutes, "NO_ROUTES", "No routes found") // verify loading state was removed and that error was displayed verify(!root.swapAdaptor.validSwapProposalReceived) @@ -633,9 +633,155 @@ Item { // verify loading state was set and no errors currently verifyLoadingAndNoErrorsState(payPanel, receivePanel) + // emit event that no routes were found due to not enough token balance + root.swapStore.suggestedRoutesReady(root.dummySwapTransactionRoutes.txNoRoutes, Constants.swap.errorCodes.errNotEnoughTokenBalance, "errNotEnoughTokenBalance") + + // verify loading state was removed and that error was displayed + verify(!root.swapAdaptor.validSwapProposalReceived) + verify(!root.swapAdaptor.swapProposalLoading) + compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "") + compare(root.swapAdaptor.swapOutputData.toTokenAmount, "") + compare(root.swapAdaptor.swapOutputData.totalFees, 0) + compare(root.swapAdaptor.swapOutputData.approvalNeeded, false) + compare(root.swapAdaptor.swapOutputData.hasError, true) + verify(errorTag.visible) + verify(errorTag.text, qsTr("Insufficient funds for swap")) + verify(!signButton.enabled) + compare(signButton.text, qsTr("Swap")) + + // verfy input and output panels + verify(!payPanel.mainInputLoading) + verify(!payPanel.bottomTextLoading) + verify(!receivePanel.mainInputLoading) + verify(!receivePanel.bottomTextLoading) + verify(!receivePanel.interactive) + compare(receivePanel.selectedHoldingId, root.swapFormData.toTokenKey) + compare(receivePanel.value, 0) + compare(receivePanel.rawValue, "0") + + // edit some params to retry swap + root.swapFormData.fromTokenAmount = "0.00012" + waitForRendering(receivePanel) + formValuesChanged.wait() + + // wait for fetchSuggestedRoutes function to be called + fetchSuggestedRoutesCalled.wait() + + // verify loading state was set and no errors currently + verifyLoadingAndNoErrorsState(payPanel, receivePanel) + + // emit event that no routes were found due to not enough eth balance + root.swapStore.suggestedRoutesReady(root.dummySwapTransactionRoutes.txNoRoutes, Constants.swap.errorCodes.errNotEnoughNativeBalance, "errNotEnoughNativeBalance") + + // verify loading state was removed and that error was displayed + verify(!root.swapAdaptor.validSwapProposalReceived) + verify(!root.swapAdaptor.swapProposalLoading) + compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "") + compare(root.swapAdaptor.swapOutputData.toTokenAmount, "") + compare(root.swapAdaptor.swapOutputData.totalFees, 0) + compare(root.swapAdaptor.swapOutputData.approvalNeeded, false) + compare(root.swapAdaptor.swapOutputData.hasError, true) + verify(errorTag.visible) + verify(errorTag.text, qsTr("Insufficient funds to pay gas fees")) + verify(!signButton.enabled) + compare(signButton.text, qsTr("Swap")) + + // verfy input and output panels + verify(!payPanel.mainInputLoading) + verify(!payPanel.bottomTextLoading) + verify(!receivePanel.mainInputLoading) + verify(!receivePanel.bottomTextLoading) + verify(!receivePanel.interactive) + compare(receivePanel.selectedHoldingId, root.swapFormData.toTokenKey) + compare(receivePanel.value, 0) + compare(receivePanel.rawValue, "0") + + // edit some params to retry swap + root.swapFormData.fromTokenAmount = "0.00013" + waitForRendering(receivePanel) + formValuesChanged.wait() + // wait for fetchSuggestedRoutes function to be called + fetchSuggestedRoutesCalled.wait() + + // wait for fetchSuggestedRoutes function to be called + fetchSuggestedRoutesCalled.wait() + + // verify loading state was set and no errors currently + verifyLoadingAndNoErrorsState(payPanel, receivePanel) + + // emit event that no routes were found due to price timeout + root.swapStore.suggestedRoutesReady(root.dummySwapTransactionRoutes.txNoRoutes, Constants.swap.errorCodes.errPriceTimeout, "errPriceTimeout") + + // verify loading state was removed and that error was displayed + verify(!root.swapAdaptor.validSwapProposalReceived) + verify(!root.swapAdaptor.swapProposalLoading) + compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "") + compare(root.swapAdaptor.swapOutputData.toTokenAmount, "") + compare(root.swapAdaptor.swapOutputData.totalFees, 0) + compare(root.swapAdaptor.swapOutputData.approvalNeeded, false) + compare(root.swapAdaptor.swapOutputData.hasError, true) + verify(errorTag.visible) + verify(errorTag.text, qsTr("Fetching the price took longer than expected. Please, try again later.")) + verify(!signButton.enabled) + compare(signButton.text, qsTr("Swap")) + + // verfy input and output panels + verify(!payPanel.mainInputLoading) + verify(!payPanel.bottomTextLoading) + verify(!receivePanel.mainInputLoading) + verify(!receivePanel.bottomTextLoading) + verify(!receivePanel.interactive) + compare(receivePanel.selectedHoldingId, root.swapFormData.toTokenKey) + compare(receivePanel.value, 0) + compare(receivePanel.rawValue, "0") + + // edit some params to retry swap + root.swapFormData.fromTokenAmount = "0.00013" + waitForRendering(receivePanel) + formValuesChanged.wait() + + // wait for fetchSuggestedRoutes function to be called + fetchSuggestedRoutesCalled.wait() + + // verify loading state was set and no errors currently + verifyLoadingAndNoErrorsState(payPanel, receivePanel) + + // emit event that no routes were found due to not enough liquidity + root.swapStore.suggestedRoutesReady(root.dummySwapTransactionRoutes.txNoRoutes, Constants.swap.errorCodes.errNotEnoughLiquidity, "errNotEnoughLiquidity") + + // verify loading state was removed and that error was displayed + verify(!root.swapAdaptor.validSwapProposalReceived) + verify(!root.swapAdaptor.swapProposalLoading) + compare(root.swapAdaptor.swapOutputData.fromTokenAmount, "") + compare(root.swapAdaptor.swapOutputData.toTokenAmount, "") + compare(root.swapAdaptor.swapOutputData.totalFees, 0) + compare(root.swapAdaptor.swapOutputData.approvalNeeded, false) + compare(root.swapAdaptor.swapOutputData.hasError, true) + verify(errorTag.visible) + verify(errorTag.text, qsTr("Not enough liquidity. Lower token amount or try again later.")) + verify(!signButton.enabled) + compare(signButton.text, qsTr("Swap")) + + // verfy input and output panels + verify(!payPanel.mainInputLoading) + verify(!payPanel.bottomTextLoading) + verify(!receivePanel.mainInputLoading) + verify(!receivePanel.bottomTextLoading) + verify(!receivePanel.interactive) + compare(receivePanel.selectedHoldingId, root.swapFormData.toTokenKey) + compare(receivePanel.value, 0) + compare(receivePanel.rawValue, "0") + + // edit some params to retry swap + root.swapFormData.fromTokenAmount = "0.00014" + waitForRendering(receivePanel) + formValuesChanged.wait() + // verify loading state was set and no errors currently + verifyLoadingAndNoErrorsState(payPanel, receivePanel) + // emit event with route that needs no approval let txRoutes = root.dummySwapTransactionRoutes.txHasRouteNoApproval - root.swapStore.suggestedRoutesReady(txRoutes) + root.swapStore.suggestedRoutesReady(txRoutes, "", "") // verify loading state removed and data is displayed as expected on the Modal verify(root.swapAdaptor.validSwapProposalReceived) @@ -684,7 +830,7 @@ Item { // emit event with route that needs no approval let txRoutes2 = root.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded - root.swapStore.suggestedRoutesReady(txRoutes2) + root.swapStore.suggestedRoutesReady(txRoutes2, "", "") // verify loading state removed and data ius displayed as expected on the Modal verify(root.swapAdaptor.validSwapProposalReceived) @@ -1330,7 +1476,7 @@ Item { // emit event with route that needs no approval let txRoutes = root.dummySwapTransactionRoutes.txHasRoutesApprovalNeeded txRoutes.uuid = root.swapAdaptor.uuid - root.swapStore.suggestedRoutesReady(txRoutes) + root.swapStore.suggestedRoutesReady(txRoutes, "", "") // calculation needed for total fees let gasTimeEstimate = txRoutes.gasTimeEstimate @@ -1426,7 +1572,7 @@ Item { let txHasRouteNoApproval = root.dummySwapTransactionRoutes.txHasRouteNoApproval txHasRouteNoApproval.uuid = root.swapAdaptor.uuid - root.swapStore.suggestedRoutesReady(txHasRouteNoApproval) + root.swapStore.suggestedRoutesReady(txHasRouteNoApproval, "", "") verify(!root.swapAdaptor.approvalPending) verify(!root.swapAdaptor.approvalSuccessful) @@ -1601,7 +1747,7 @@ Item { // emit routes ready let txHasRouteNoApproval = root.dummySwapTransactionRoutes.txHasRouteNoApproval txHasRouteNoApproval.uuid = root.swapAdaptor.uuid - root.swapStore.suggestedRoutesReady(txHasRouteNoApproval) + root.swapStore.suggestedRoutesReady(txHasRouteNoApproval, "", "") // check if fetch occurs automatically after 15 seconds fetchSuggestedRoutesCalled.wait() diff --git a/storybook/stubs/shared/stores/send/TransactionStore.qml b/storybook/stubs/shared/stores/send/TransactionStore.qml index f412a4f3db..7770a29b1e 100644 --- a/storybook/stubs/shared/stores/send/TransactionStore.qml +++ b/storybook/stubs/shared/stores/send/TransactionStore.qml @@ -48,7 +48,7 @@ QtObject { readonly property QtObject walletSectionSendInst: QtObject { signal transactionSent(var chainId, var txHash, var uuid, var error) - signal suggestedRoutesReady(var txRoutes) + signal suggestedRoutesReady(var txRoutes, string errCode, string errDescription) } readonly property QtObject mainModuleInst: QtObject { signal resolvedENS(var resolvedPubKey, var resolvedAddress, var uuid) diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml index 5b2cf504e9..34475e1dcd 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml @@ -55,6 +55,8 @@ StatusDialog { root.swapAdaptor.swapOutputData.resetPathInfoAndError() debounceFetchSuggestedRoutes() } + + readonly property bool isError: root.swapAdaptor.errorMessage !== "" } Connections { @@ -227,6 +229,9 @@ StatusDialog { root.swapInputParamsForm.fromTokenAmount = amount } } + onAmountEnteredGreaterThanBalanceChanged: { + root.swapAdaptor.amountEnteredGreaterThanBalance = payPanel.amountEnteredGreaterThanBalance + } } SwapInputPanel { @@ -301,18 +306,12 @@ StatusDialog { ErrorTag { objectName: "errorTag" - visible: root.swapAdaptor.swapOutputData.hasError || payPanel.amountEnteredGreaterThanBalance + visible: d.isError Layout.alignment: Qt.AlignHCenter Layout.topMargin: Style.current.smallPadding - text: { - if (root.swapAdaptor.swapOutputData.hasError) { - return qsTr("An error has occured, please try again") - } else if (payPanel.amountEnteredGreaterThanBalance) { - return qsTr("Insufficient funds for swap") - } - } - buttonText: qsTr("Buy crypto") - buttonVisible: !root.swapAdaptor.swapOutputData.hasError && payPanel.amountEnteredGreaterThanBalance + text: root.swapAdaptor.errorMessage + buttonText: root.swapAdaptor.isTokenBalanceInsufficient ? qsTr("Buy crypto") : qsTr("Buy ETH") + buttonVisible: visible && (root.swapAdaptor.isTokenBalanceInsufficient || root.swapAdaptor.isEthBalanceInsufficient) onButtonClicked: Global.openBuyCryptoModalRequested() } } @@ -407,7 +406,7 @@ StatusDialog { disabledColor: Theme.palette.directColor8 enabled: root.swapAdaptor.validSwapProposalReceived && editSlippagePanel.valid && - !payPanel.amountEnteredGreaterThanBalance && + !d.isError && !root.swapAdaptor.approvalPending onClicked: { if (root.swapAdaptor.validSwapProposalReceived) { diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml index 5ee5921329..d581b7b79e 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml @@ -27,6 +27,9 @@ QObject { property bool approvalPending: false property bool approvalSuccessful: false + // the below property holds internal checks done by the SwapModal + property bool amountEnteredGreaterThanBalance: false + // To expose the selected from and to Token from the SwapModal readonly property var fromToken: fromTokenEntry.item readonly property var toToken: toTokenEntry.item @@ -89,6 +92,10 @@ QObject { filters: ValueFilter { roleName: "isTest"; value: root.swapStore.areTestNetworksEnabled } } + readonly property string errorMessage: d.errorMessage + readonly property bool isEthBalanceInsufficient: d.isEthBalanceInsufficient + readonly property bool isTokenBalanceInsufficient: d.isTokenBalanceInsufficient + signal suggestedRoutesReady() QtObject { @@ -141,6 +148,45 @@ QObject { formattedBalance: "0 %1".arg(root.fromToken.symbol) } } + + // Properties to handle error states + readonly property bool isRouteEthBalanceInsufficient: root.validSwapProposalReceived && root.swapOutputData.errCode === Constants.swap.errorCodes.errNotEnoughNativeBalance + + readonly property bool isRouteTokenBalanceInsufficient: root.validSwapProposalReceived && root.swapOutputData.errCode === Constants.swap.errorCodes.errNotEnoughTokenBalance + + readonly property bool isTokenBalanceInsufficient: { + return (root.amountEnteredGreaterThanBalance || isRouteTokenBalanceInsufficient) && + root.fromToken.symbol !== Constants.ethToken + } + + readonly property bool isEthBalanceInsufficient: { + return (root.amountEnteredGreaterThanBalance && root.fromToken.symbol === Constants.ethToken) || + isRouteEthBalanceInsufficient + } + + readonly property bool isBalanceInsufficientForSwap: { + return (root.amountEnteredGreaterThanBalance && root.fromToken.symbol === Constants.ethToken) || + (isTokenBalanceInsufficient && root.fromToken.symbol !== Constants.ethToken) + } + + readonly property bool isBalanceInsufficientForFees: !isBalanceInsufficientForSwap && isEthBalanceInsufficient + + property string errorMessage: { + if (isBalanceInsufficientForSwap) { + return qsTr("Insufficient funds for swap") + } else if (isBalanceInsufficientForFees) { + return qsTr("Insufficient funds to pay gas fees") + } else if (root.swapOutputData.hasError) { + switch (root.swapOutputData.errCode) { + case Constants.swap.errorCodes.errPriceTimeout: + return qsTr("Fetching the price took longer than expected. Please, try again later.") + case Constants.swap.errorCodes.errNotEnoughLiquidity: + return qsTr("Not enough liquidity. Lower token amount or try again later.") + } + return qsTr("Something went wrong. Change amount, token or try again later.") + } + return "" + } } ModelEntry { @@ -166,7 +212,7 @@ QObject { Connections { target: root.swapStore - function onSuggestedRoutesReady(txRoutes) { + function onSuggestedRoutesReady(txRoutes, errCode, errDescription) { if (txRoutes.uuid !== d.uuid) { // Suggested routes for a different fetch, ignore return @@ -175,8 +221,10 @@ QObject { root.validSwapProposalReceived = false root.swapProposalLoading = false root.swapOutputData.rawPaths = txRoutes.rawPaths + root.swapOutputData.errCode = errCode + root.swapOutputData.errDescription = errDescription // if valid route was found - if(txRoutes.suggestedRoutes.count === 1) { + if(txRoutes.suggestedRoutes.count > 0) { root.validSwapProposalReceived = true root.swapOutputData.toTokenAmount = AmountsArithmetic.div(AmountsArithmetic.fromString(txRoutes.amountToReceive), AmountsArithmetic.fromNumber(1, root.toToken.decimals)).toString() @@ -190,12 +238,12 @@ QObject { root.swapOutputData.approvalGasFees = !!bestPath ? bestPath.approvalGasFees.toString() : "" root.swapOutputData.approvalAmountRequired = !!bestPath ? bestPath.approvalAmountRequired: "" root.swapOutputData.approvalContractAddress = !!bestPath ? bestPath.approvalContractAddress: "" - root.swapOutputData.estimatedTime = !!bestPath ? bestPath.estimatedTime: Constants.TransactionEstimatedTime.Unknown + root.swapOutputData.estimatedTime = !!bestPath ? bestPath.estimatedTime: Constants.TransactionEstimatedTime.Unknown root.swapOutputData.txProviderName = !!bestPath ? bestPath.bridgeName: "" - } - else { + } else { root.swapOutputData.hasError = true } + root.swapOutputData.hasError = root.swapOutputData.hasError || root.swapOutputData.errCode !== "" root.suggestedRoutesReady() } diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapOutputData.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapOutputData.qml index f02d6c8c6a..9d817f5092 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapOutputData.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapOutputData.qml @@ -12,6 +12,8 @@ QtObject { // TODO: this should be string but backend gas_estimate_item.nim passes this as float property real totalFees: 0 property bool hasError + property string errCode + property string errDescription property var rawPaths: [] // need to check how this is done in new router v2, right now it is Enum type property int estimatedTime @@ -23,6 +25,8 @@ QtObject { function resetPathInfoAndError() { root.hasError = false + root.errCode = "" + root.errDescription = "" root.rawPaths = [] } diff --git a/ui/app/AppLayouts/Wallet/stores/SwapStore.qml b/ui/app/AppLayouts/Wallet/stores/SwapStore.qml index db52fc05cb..35addca1e4 100644 --- a/ui/app/AppLayouts/Wallet/stores/SwapStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/SwapStore.qml @@ -16,14 +16,14 @@ QtObject { Remove these and use the new TransactorStore in SwapModalAdaptor when that happens. */ readonly property var walletSectionSendInst: walletSectionSend - signal suggestedRoutesReady(var txRoutes) + signal suggestedRoutesReady(var txRoutes, string errCode, string errDescription) signal transactionSent(var chainId, var txHash, var uuid, var error) signal transactionSendingComplete(var txHash, var success) readonly property Connections walletSectionSendConnections: Connections { target: root.walletSectionSendInst - function onSuggestedRoutesReady(txRoutes) { - root.suggestedRoutesReady(txRoutes) + function onSuggestedRoutesReady(txRoutes, errCode, errDescription) { + root.suggestedRoutesReady(txRoutes, errCode, errDescription) } function onTransactionSent(chainId, txHash, uuid, error) { root.transactionSent(chainId, txHash, uuid, error) diff --git a/ui/imports/shared/popups/send/SendModal.qml b/ui/imports/shared/popups/send/SendModal.qml index 521349e071..1a30e203b9 100644 --- a/ui/imports/shared/popups/send/SendModal.qml +++ b/ui/imports/shared/popups/send/SendModal.qml @@ -644,7 +644,7 @@ StatusDialog { Connections { target: popup.store.walletSectionSendInst - function onSuggestedRoutesReady(txRoutes) { + function onSuggestedRoutesReady(txRoutes, errCode, errDescription) { popup.bestRoutes = txRoutes.suggestedRoutes let gasTimeEstimate = txRoutes.gasTimeEstimate d.totalTimeEstimate = WalletUtils.getLabelForEstimatedTxTime(gasTimeEstimate.totalTime) diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 3ba68169ee..665b972552 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -1335,6 +1335,13 @@ QtObject { this list dynamically */ readonly property string paraswapIcon: "paraswap" readonly property string paraswapUrl: "app.paraswap.io" + + readonly property QtObject errorCodes: QtObject { + readonly property string errNotEnoughTokenBalance: "WR-016" + readonly property string errNotEnoughNativeBalance: "WR-017" + readonly property string errPriceTimeout: "WPP-037" + readonly property string errNotEnoughLiquidity: "WPP-038" + } } // Mirrors src/app_service/service/transaction/service.nim -> EstimatedTime diff --git a/vendor/status-go b/vendor/status-go index d07f9b5b16..afc6e7bcb9 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit d07f9b5b162c078f71bf19a189e2e26c118a0db4 +Subproject commit afc6e7bcb9e70e8ece8744dfab1b23ba5d087e6e