diff --git a/client/cypress/e2e/rewardsCalculator.cy.ts b/client/cypress/e2e/rewardsCalculator.cy.ts index 6468009b7d..caf26adec4 100644 --- a/client/cypress/e2e/rewardsCalculator.cy.ts +++ b/client/cypress/e2e/rewardsCalculator.cy.ts @@ -5,15 +5,6 @@ import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { describe(`rewards calculator: ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - beforeEach(() => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); @@ -35,7 +26,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); } - it('clicking on rewards calculator icon opens rewards calcultor modal', () => { + it('clicking on rewards calculator icon opens rewards calculator modal', () => { cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); cy.get('[data-test=ModalRewardsCalculator]').should('be.visible'); }); @@ -201,5 +192,30 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes .invoke('val') .should('eq', ''); }); + + it('Closing the modal successfully cancels the request /estimated_budget', () => { + cy.intercept('POST', '/rewards/estimated_budget', { + body: { budget: '850684931506849269541' }, + // Long enough to never complete. + delay: 5000000, + }).as('postEstimatedRewards'); + + cy.window().then(win => { + cy.spy(win.console, 'error').as('consoleErrSpy'); + }); + + cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); + + cy.get('[data-test=RewardsCalculator__InputText--estimatedRewards--crypto__Loader]').should( + 'be.visible', + ); + + cy.get('[data-test=ModalRewardsCalculator__Button]').click(); + cy.get('[data-test=ModalRewardsCalculator').should('not.be.visible'); + + cy.on('uncaught:exception', error => { + expect(error.code).to.equal('ERR_CANCELED'); + }); + }); }); }); diff --git a/client/src/api/calls/calculateRewards.ts b/client/src/api/calls/calculateRewards.ts index 97852d8d84..11aaa849de 100644 --- a/client/src/api/calls/calculateRewards.ts +++ b/client/src/api/calls/calculateRewards.ts @@ -1,3 +1,5 @@ +import { GenericAbortSignal } from 'axios'; + import env from 'env'; import apiService from 'services/apiService'; @@ -9,7 +11,7 @@ export function apiPostCalculateRewards( // WEI amount: string, days: number, - signal?: AbortSignal, + signal: GenericAbortSignal, ): Promise { return apiService .post( diff --git a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculator.tsx b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculator.tsx index 971ea9650b..643a0a7b17 100644 --- a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculator.tsx +++ b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculator.tsx @@ -1,15 +1,12 @@ import cx from 'classnames'; import { useFormik } from 'formik'; import debounce from 'lodash/debounce'; -import React, { FC, useCallback, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { apiPostCalculateRewards } from 'api/calls/calculateRewards'; -import clientReactQuery from 'api/clients/client-react-query'; -import { QUERY_KEYS, ROOTS } from 'api/queryKeys'; import BoxRounded from 'components/ui/BoxRounded'; import InputText from 'components/ui/InputText'; -import { GLM_TOTAL_SUPPLY } from 'constants/currencies'; +import useCalculateRewards from 'hooks/mutations/useCalculateRewards'; import useCryptoValues from 'hooks/queries/useCryptoValues'; import i18n from 'i18n'; import useSettingsStore from 'store/settings/store'; @@ -27,8 +24,6 @@ const EarnRewardsCalculator: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'components.dedicated.rewardsCalculator', }); - const [estimatedRewards, setEstimatedRewards] = useState(); - const [isFetching, setIsFetching] = useState(false); const { data: { displayCurrency, isCryptoMainValueDisplay }, } = useSettingsStore(({ data }) => ({ @@ -38,44 +33,29 @@ const EarnRewardsCalculator: FC = () => { }, })); const { data: cryptoValues } = useCryptoValues(displayCurrency); + const { + data: calculateRewards, + mutateAsync: mutateAsyncRewardsCalculator, + reset: resetCalculateRewards, + isPending: isPendingCalculateRewards, + } = useCalculateRewards(); // eslint-disable-next-line react-hooks/exhaustive-deps const fetchEstimatedRewardsDebounced = useCallback( - debounce((amountGlm, d) => { - const isFetchingRewards = clientReactQuery.isFetching({ queryKey: [ROOTS.calculateRewards] }); - - if (!amountGlm || !d || parseUnitsBigInt(amountGlm) > GLM_TOTAL_SUPPLY) { - if (isFetchingRewards) { - clientReactQuery.cancelQueries({ queryKey: [ROOTS.calculateRewards] }); - } - setEstimatedRewards(undefined); - setIsFetching(false); - return; - } - - if (isFetchingRewards) { - clientReactQuery.cancelQueries({ queryKey: [ROOTS.calculateRewards] }); - } - + debounce(({ amountGlm, numberOfDays }) => { const amountGlmWEI = formatUnitsBigInt(parseUnitsBigInt(amountGlm, 'ether'), 'wei'); - setIsFetching(true); - - clientReactQuery - .fetchQuery({ - queryFn: ({ signal }) => apiPostCalculateRewards(amountGlmWEI, parseInt(d, 10), signal), - queryKey: QUERY_KEYS.calculateRewards(amountGlmWEI, parseInt(d, 10)), - }) - .then(res => { - setIsFetching(false); - setEstimatedRewards(parseUnitsBigInt(res.budget, 'wei')); - }); + const numberOfDaysNumber = parseInt(numberOfDays, 10); + + resetCalculateRewards(); + mutateAsyncRewardsCalculator({ amountGlm: amountGlmWEI, numberOfDays: numberOfDaysNumber }); }, 300), [], ); const formik = useFormik({ initialValues: formInitialValues, - onSubmit: values => fetchEstimatedRewardsDebounced(values.valueCrypto, values.days), + onSubmit: values => + fetchEstimatedRewardsDebounced({ amountGlm: values.valueCrypto, numberOfDays: values.days }), validateOnChange: true, validationSchema: validationSchema(t), }); @@ -99,19 +79,29 @@ const EarnRewardsCalculator: FC = () => { useEffect(() => { formik.validateForm().then(() => { - fetchEstimatedRewardsDebounced(formik.values.valueCrypto, formik.values.days); + fetchEstimatedRewardsDebounced({ + amountGlm: formik.values.valueCrypto, + numberOfDays: formik.values.days, + }); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [formik.values.valueCrypto, formik.values.days]); + useEffect(() => { + return () => { + resetCalculateRewards(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const estimatedFormattedRewardsValue: FormattedCryptoValue = - formik.values.valueCrypto && formik.values.days && estimatedRewards - ? getFormattedEthValue(estimatedRewards) + formik.values.valueCrypto && formik.values.days && calculateRewards + ? getFormattedEthValue(parseUnitsBigInt(calculateRewards.budget, 'wei')) : { - fullString: '', - suffix: 'ETH', - value: '', - }; + fullString: '', + suffix: 'ETH', + value: '', + }; const cryptoFiatRatio = cryptoValues?.ethereum[displayCurrency || 'usd'] || 1; const fiat = estimatedFormattedRewardsValue.value @@ -155,7 +145,7 @@ const EarnRewardsCalculator: FC = () => { isButtonClearVisible={false} isDisabled shouldAutoFocusAndSelect={false} - showLoader={isFetching} + showLoader={isPendingCalculateRewards} suffix={estimatedFormattedRewardsValue.suffix} suffixClassName={styles.estimatedRewardsSuffix} value={estimatedFormattedRewardsValue.value} @@ -169,7 +159,7 @@ const EarnRewardsCalculator: FC = () => { isButtonClearVisible={false} isDisabled shouldAutoFocusAndSelect={false} - showLoader={isFetching} + showLoader={isPendingCalculateRewards} suffix={displayCurrency.toUpperCase()} suffixClassName={styles.estimatedRewardsSuffix} value={fiat} diff --git a/client/src/hooks/mutations/useAllocateSimulate.ts b/client/src/hooks/mutations/useAllocateSimulate.ts index 0a9dc9eba0..53f5d00251 100644 --- a/client/src/hooks/mutations/useAllocateSimulate.ts +++ b/client/src/hooks/mutations/useAllocateSimulate.ts @@ -1,5 +1,5 @@ import { useMutation, UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; -import { useCallback, useRef } from 'react'; +import { useRef } from 'react'; import { useAccount } from 'wagmi'; import { apiPostAllocateLeverage, ApiPostAllocateLeverageResponse } from 'api/calls/allocate'; @@ -26,10 +26,10 @@ export default function useAllocateSimulate( ...options, }); - const reset = useCallback(() => { + const reset = () => { abortControllerRef.current?.abort(); mutation.reset(); - }, [abortControllerRef, mutation]); + }; return { ...mutation, reset }; } diff --git a/client/src/hooks/mutations/useCalculateRewards.ts b/client/src/hooks/mutations/useCalculateRewards.ts new file mode 100644 index 0000000000..6cc0669df5 --- /dev/null +++ b/client/src/hooks/mutations/useCalculateRewards.ts @@ -0,0 +1,30 @@ +import { useMutation, UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; +import { useRef } from 'react'; + +import { PostCalculateRewardsResponse, apiPostCalculateRewards } from 'api/calls/calculateRewards'; + +type CalculateRewardsArguments = { + amountGlm: string; + numberOfDays: number; +}; + +export default function useCalculateRewards( + options?: UseMutationOptions, +): UseMutationResult { + const abortControllerRef = useRef(null); + + const mutation = useMutation({ + mutationFn: async ({ amountGlm, numberOfDays }) => { + abortControllerRef.current = new AbortController(); + return apiPostCalculateRewards(amountGlm, numberOfDays, abortControllerRef.current.signal); + }, + ...options, + }); + + const reset = () => { + abortControllerRef.current?.abort(); + mutation.reset(); + }; + + return { ...mutation, reset }; +}