Skip to content
This repository has been archived by the owner on Jun 24, 2022. It is now read-only.

Commit

Permalink
Add fast price fetching (#2477)
Browse files Browse the repository at this point in the history
* Add fast price fetching

* Add best quote loader

* Loader style update

* Change quote loader to old cow loader

* Fix for potential race condition issue

* PR updates
  • Loading branch information
nenadV91 authored Feb 28, 2022
1 parent 90e6843 commit 588ccf2
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 32 deletions.
3 changes: 2 additions & 1 deletion src/custom/api/gnosisProtocol/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ async function _handleQuoteResponse<T = any, P extends QuoteQuery = QuoteQuery>(
}

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 = {
Expand All @@ -320,6 +320,7 @@ function _mapNewToLegacyParams(params: FeeQuoteParams): QuoteQuery {
appData: getAppDataHash(),
validTo,
partiallyFillable: false,
priceQuality,
}

const finalParams: QuoteQuery =
Expand Down
16 changes: 13 additions & 3 deletions src/custom/components/ArrowWrapperLoader/index.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 (
<Wrapper showloader={showLoader} onClick={handleClick}>
<ArrowDownIcon />
Expand Down
1 change: 1 addition & 0 deletions src/custom/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
63 changes: 42 additions & 21 deletions src/custom/hooks/useRefetchPriceCallback.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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'
Expand Down Expand Up @@ -109,6 +109,7 @@ export function handleQuoteError({ quoteData, error, addUnsupportedToken }: Hand
}

const getBestQuoteResolveOnlyLastCall = onlyResolvesLast<QuoteResult>(getBestQuote)
const getFastQuoteResolveOnlyLastCall = onlyResolvesLast<QuoteResult>(getFastQuote)

/**
* @returns callback that fetches a new quote and update the state
Expand Down Expand Up @@ -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<QuoteResult>, 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)
Expand Down Expand Up @@ -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({
Expand All @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/custom/state/price/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export const useIsQuoteLoading = () =>
return state.price.loading
})

export const useIsBestQuoteLoading = () =>
useSelector<AppState, boolean>((state) => {
return state.price.loadingBestQuote
})

interface UseGetQuoteAndStatus {
quote?: QuoteInformationObject
isGettingNewQuote: boolean
Expand Down
29 changes: 22 additions & 7 deletions src/custom/state/price/reducer.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -27,9 +27,9 @@ export type QuoteInformationState = {
readonly [chainId in ChainId]?: Partial<QuotesMap>
}

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(
Expand Down Expand Up @@ -82,8 +82,9 @@ export default createReducer(initialState, (builder) =>
validTo,
}

// Activate loader
// Activate loaders
state.loading = true
state.loadingBestQuote = true
})

/**
Expand Down Expand Up @@ -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
}
})

/**
Expand All @@ -147,7 +161,8 @@ export default createReducer(initialState, (builder) =>
}
}

// Stop the loader
// Stop the loaders
state.loading = false
state.loadingBestQuote = false
})
)
8 changes: 8 additions & 0 deletions src/custom/utils/price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export type FeeQuoteParams = Pick<OrderMetaData, 'sellToken' | 'buyToken' | 'kin
userAddress?: string | null
receiver?: string | null
validTo: number
priceQuality?: string
isBestQuote?: boolean
}

export type PriceQuoteParams = Omit<FeeQuoteParams, 'sellToken' | 'buyToken'> & {
Expand Down Expand Up @@ -371,6 +373,12 @@ export async function getBestQuote({
}
}

export async function getFastQuote({ quoteParams }: QuoteParams): Promise<QuoteResult> {
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)
Expand Down

0 comments on commit 588ccf2

Please sign in to comment.