diff --git a/src/custom/api/gnosisProtocol/api.ts b/src/custom/api/gnosisProtocol/api.ts index 8c15423c3..e8c0abbae 100644 --- a/src/custom/api/gnosisProtocol/api.ts +++ b/src/custom/api/gnosisProtocol/api.ts @@ -308,7 +308,7 @@ async function _handleQuoteResponse( } function _mapNewToLegacyParams(params: FeeQuoteParams): QuoteQuery { - const { amount, kind, userAddress, receiver, validTo, sellToken, buyToken, chainId } = params + const { amount, kind, userAddress, receiver, validTo, sellToken, buyToken, chainId, priceQuality } = params const fallbackAddress = userAddress || ZERO_ADDRESS const baseParams = { @@ -320,6 +320,7 @@ function _mapNewToLegacyParams(params: FeeQuoteParams): QuoteQuery { appData: getAppDataHash(), validTo, partiallyFillable: false, + priceQuality, } const finalParams: QuoteQuery = diff --git a/src/custom/components/ArrowWrapperLoader/index.tsx b/src/custom/components/ArrowWrapperLoader/index.tsx index bc18da974..e81628ede 100644 --- a/src/custom/components/ArrowWrapperLoader/index.tsx +++ b/src/custom/components/ArrowWrapperLoader/index.tsx @@ -1,9 +1,10 @@ +import { useMemo } from 'react' import styled from 'styled-components/macro' import loadingCowGif from 'assets/cow-swap/cow-load.gif' import { ArrowDown } from 'react-feather' import useLoadingWithTimeout from 'hooks/useLoadingWithTimeout' -import { useIsQuoteRefreshing } from 'state/price/hooks' -import { LONG_LOAD_THRESHOLD } from 'constants/index' +import { useIsQuoteRefreshing, useIsBestQuoteLoading } from 'state/price/hooks' +import { LONG_LOAD_THRESHOLD, SHORT_LOAD_THRESHOLD } from 'constants/index' interface ShowLoaderProp { showloader: boolean @@ -120,12 +121,21 @@ export interface ArrowWrapperLoaderProps { export function ArrowWrapperLoader({ onSwitchTokens, setApprovalSubmitted }: ArrowWrapperLoaderProps) { const isRefreshingQuote = useIsQuoteRefreshing() - const showLoader = useLoadingWithTimeout(isRefreshingQuote, LONG_LOAD_THRESHOLD) + const isBestQuoteLoading = useIsBestQuoteLoading() + + const showCowLoader = useLoadingWithTimeout(isRefreshingQuote, LONG_LOAD_THRESHOLD) + const showQuoteLoader = useLoadingWithTimeout(isBestQuoteLoading, SHORT_LOAD_THRESHOLD) + const handleClick = () => { setApprovalSubmitted(false) // reset 2 step UI for approvals onSwitchTokens() } + const showLoader = useMemo( + () => Boolean(loadingCowGif) && (showCowLoader || showQuoteLoader), + [showCowLoader, showQuoteLoader] + ) + return ( diff --git a/src/custom/constants/index.ts b/src/custom/constants/index.ts index 6eee8b028..bdf791c07 100644 --- a/src/custom/constants/index.ts +++ b/src/custom/constants/index.ts @@ -19,6 +19,7 @@ export const FULL_PRICE_PRECISION = 20 export const FIAT_PRECISION = 2 export const PERCENTAGE_PRECISION = 2 +export const SHORT_LOAD_THRESHOLD = 500 export const LONG_LOAD_THRESHOLD = 2000 export const APP_DATA_HASH = getAppDataHash() diff --git a/src/custom/hooks/useRefetchPriceCallback.tsx b/src/custom/hooks/useRefetchPriceCallback.tsx index 1ed59b682..19966da66 100644 --- a/src/custom/hooks/useRefetchPriceCallback.tsx +++ b/src/custom/hooks/useRefetchPriceCallback.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' -import { FeeQuoteParams, getBestQuote, QuoteParams, QuoteResult } from 'utils/price' +import { FeeQuoteParams, getBestQuote, getFastQuote, QuoteParams, QuoteResult } from 'utils/price' import { isValidOperatorError, ApiErrorCodes } from 'api/gnosisProtocol/errors/OperatorError' import GpQuoteError, { GpQuoteErrorCodes, @@ -19,7 +19,7 @@ import { QuoteInformationObject } from 'state/price/reducer' import { useQuoteDispatchers } from 'state/price/hooks' import { AddGpUnsupportedTokenParams } from 'state/lists/actions' import { QuoteError } from 'state/price/actions' -import { onlyResolvesLast } from 'utils/async' +import { CancelableResult, onlyResolvesLast } from 'utils/async' import useGetGpPriceStrategy from 'hooks/useGetGpPriceStrategy' import { calculateValidTo } from 'hooks/useSwapCallback' import { useUserTransactionTTL } from 'state/user/hooks' @@ -109,6 +109,7 @@ export function handleQuoteError({ quoteData, error, addUnsupportedToken }: Hand } const getBestQuoteResolveOnlyLastCall = onlyResolvesLast(getBestQuote) +const getFastQuoteResolveOnlyLastCall = onlyResolvesLast(getFastQuote) /** * @returns callback that fetches a new quote and update the state @@ -140,24 +141,10 @@ export function useRefetchQuoteCallback() { let quoteData: FeeQuoteParams | QuoteInformationObject = quoteParams - const { sellToken, buyToken, chainId } = quoteData - try { - // Start action: Either new quote or refreshing quote - if (isPriceRefresh) { - // Refresh the quote - refreshQuote({ sellToken, chainId }) - } else { - // Get new quote - getNewQuote(quoteParams) - } - - registerOnWindow({ - getBestQuote: async () => getBestQuoteResolveOnlyLastCall({ ...params, strategy: priceStrategy }), - }) + // price can be null if fee > price + const handleResponse = (response: CancelableResult, isBestQuote: boolean) => { + const { cancelled, data } = response - // Get the quote - // price can be null if fee > price - const { cancelled, data } = await getBestQuoteResolveOnlyLastCall({ ...params, strategy: priceStrategy }) if (cancelled) { // Cancellation can happen if a new request is made, then any ongoing query is canceled console.debug('[useRefetchPriceCallback] Canceled get quote price for', params) @@ -201,8 +188,10 @@ export function useRefetchQuoteCallback() { } // Update quote - updateQuote(quoteData) - } catch (error) { + updateQuote({ ...quoteData, isBestQuote }) + } + + const handleError = (error: Error) => { // handle any errors in quote fetch // we re-use the quoteData object in scope to save values into state const quoteError = handleQuoteError({ @@ -217,6 +206,38 @@ export function useRefetchQuoteCallback() { error: quoteError, }) } + + const { sellToken, buyToken, chainId } = quoteData + // Start action: Either new quote or refreshing quote + if (isPriceRefresh) { + // Refresh the quote + refreshQuote({ sellToken, chainId }) + } else { + // Get new quote + getNewQuote(quoteParams) + } + + // Init get quote methods params + const bestQuoteParams = { ...params, strategy: priceStrategy } + const fastQuoteParams = { quoteParams: { ...quoteParams, priceQuality: 'fast' } } + + // Register get best and fast quote methods on window + registerOnWindow({ + getBestQuote: async () => getBestQuoteResolveOnlyLastCall(bestQuoteParams), + getFastQuote: async () => getFastQuoteResolveOnlyLastCall(fastQuoteParams), + }) + + // Get the fast quote + if (!isPriceRefresh) { + getFastQuoteResolveOnlyLastCall(fastQuoteParams) + .then((res) => handleResponse(res, false)) + .catch(handleError) + } + + // Get the best quote + getBestQuoteResolveOnlyLastCall(bestQuoteParams) + .then((res) => handleResponse(res, true)) + .catch(handleError) }, [ deadline, diff --git a/src/custom/state/price/hooks.ts b/src/custom/state/price/hooks.ts index a81e85980..899a5490c 100644 --- a/src/custom/state/price/hooks.ts +++ b/src/custom/state/price/hooks.ts @@ -52,6 +52,11 @@ export const useIsQuoteLoading = () => return state.price.loading }) +export const useIsBestQuoteLoading = () => + useSelector((state) => { + return state.price.loadingBestQuote + }) + interface UseGetQuoteAndStatus { quote?: QuoteInformationObject isGettingNewQuote: boolean diff --git a/src/custom/state/price/reducer.ts b/src/custom/state/price/reducer.ts index 6ab87869b..5f6ac9baf 100644 --- a/src/custom/state/price/reducer.ts +++ b/src/custom/state/price/reducer.ts @@ -1,4 +1,4 @@ -import { createReducer, PayloadAction } from '@reduxjs/toolkit' +import { createReducer, PayloadAction, current } from '@reduxjs/toolkit' import { SupportedChainId as ChainId } from 'constants/chains' import { OrderKind } from '@gnosis.pm/gp-v2-contracts' import { updateQuote, setQuoteError, getNewQuote, refreshQuote, QuoteError } from './actions' @@ -27,9 +27,9 @@ export type QuoteInformationState = { readonly [chainId in ChainId]?: Partial } -type InitialState = { loading: boolean; quotes: QuoteInformationState } +type InitialState = { loading: boolean; loadingBestQuote: boolean; quotes: QuoteInformationState } -const initialState: InitialState = { loading: false, quotes: {} } +const initialState: InitialState = { loadingBestQuote: false, loading: false, quotes: {} } // Makes sure there stat is initialized function initializeState( @@ -82,8 +82,9 @@ export default createReducer(initialState, (builder) => validTo, } - // Activate loader + // Activate loaders state.loading = true + state.loadingBestQuote = true }) /** @@ -115,17 +116,30 @@ export default createReducer(initialState, (builder) => .addCase(updateQuote, (state, action) => { const quotes = state.quotes const payload = action.payload - const { sellToken, chainId } = payload + const { sellToken, chainId, isBestQuote } = payload initializeState(quotes, action) // Updates the new price const quoteInformation = quotes[chainId][sellToken] - if (quoteInformation) { + const quote = current(state).quotes[chainId] + + // Flag to not update the quote when the there is already a quote price and the + // current quote in action is not the best quote, meaning the best quote for + // some reason was already loaded before fast quote and we want to keep best quote data + const hasPrice = !!quote && !!quote[sellToken]?.price?.amount + const shouldUpdate = !(!isBestQuote && hasPrice) + + if (quoteInformation && shouldUpdate) { quotes[chainId][sellToken] = { ...quoteInformation, ...payload } } // Stop the loader state.loading = false + + // Stop the quote loader when the "best" quote is fetched + if (isBestQuote) { + state.loadingBestQuote = false + } }) /** @@ -147,7 +161,8 @@ export default createReducer(initialState, (builder) => } } - // Stop the loader + // Stop the loaders state.loading = false + state.loadingBestQuote = false }) ) diff --git a/src/custom/utils/price.ts b/src/custom/utils/price.ts index d17b24338..d2a8c5392 100644 --- a/src/custom/utils/price.ts +++ b/src/custom/utils/price.ts @@ -77,6 +77,8 @@ export type FeeQuoteParams = Pick & { @@ -371,6 +373,12 @@ export async function getBestQuote({ } } +export async function getFastQuote({ quoteParams }: QuoteParams): Promise { + console.debug('[GP PRICE::API] getFastQuote - Attempting fast quote retrieval, hang tight.') + + return getFullQuote({ quoteParams }) +} + export function getValidParams(params: PriceQuoteParams) { const { baseToken: baseTokenAux, quoteToken: quoteTokenAux, chainId } = params const baseToken = toErc20Address(baseTokenAux, chainId)