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

Add fast price fetching #2477

Merged
merged 7 commits into from
Feb 28, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -319,6 +319,7 @@ function _mapNewToLegacyParams(params: FeeQuoteParams): QuoteQuery {
appData: getAppDataHash(),
validTo,
partiallyFillable: false,
priceQuality,
}

const finalParams: QuoteQuery =
Expand Down
Binary file added src/custom/assets/cow-swap/quote-load.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 30 additions & 6 deletions src/custom/components/ArrowWrapperLoader/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { useMemo } from 'react'
import styled from 'styled-components/macro'
import loadingCowGif from 'assets/cow-swap/cow-load.gif'
import loadingQuoteGif from 'assets/cow-swap/quote-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
noPadding?: boolean
}

const ArrowDownIcon = styled(ArrowDown)`
Expand Down Expand Up @@ -63,7 +66,7 @@ export const Wrapper = styled.div<ShowLoaderProp>`
> div > img {
height: 100%;
width: 100%;
padding: 2px 2px 0;
padding: ${({ noPadding }) => (noPadding ? `0px` : `2px 2px 0`)};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious why we need this? u end up leaving the nose picking GIF right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, reverted this also

object-fit: contain;
object-position: bottom;
}
Expand Down Expand Up @@ -120,18 +123,39 @@ 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 loaderGif = useMemo(() => {
let loaderGif = ''

if (showQuoteLoader) {
loaderGif = loadingQuoteGif
} else if (showCowLoader) {
loaderGif = loadingCowGif
}

return loaderGif
}, [showCowLoader, showQuoteLoader])

const showLoader = useMemo(
() => Boolean(loaderGif) && (showCowLoader || showQuoteLoader),
[loaderGif, showCowLoader, showQuoteLoader]
)

return (
<Wrapper showloader={showLoader} onClick={handleClick}>
<Wrapper noPadding={showQuoteLoader} showloader={showLoader} onClick={handleClick}>
<ArrowDownIcon />
{showLoader && (
<div>
<img src={loadingCowGif} alt="Loading prices..." />
<img src={loaderGif} alt="Loading prices..." />
</div>
)}
</Wrapper>
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
62 changes: 42 additions & 20 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 All @@ -170,6 +157,7 @@ export function useRefetchQuoteCallback() {
...quoteParams,
fee: getPromiseFulfilledValue(fee, undefined),
price: getPromiseFulfilledValue(price, undefined),
isBestQuote,
}
// check the promise fulfilled values
// handle if rejected
Expand Down Expand Up @@ -202,7 +190,9 @@ export function useRefetchQuoteCallback() {

// Update quote
updateQuote(quoteData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ALWAY update the price? no matter if its worse or better?

Copy link
Contributor Author

@nenadV91 nenadV91 Feb 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the comment up ☝️

} catch (error) {
}

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 +207,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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can there be a race condition?

getFastQuoteResolveOnlyLastCall is sent in parallel to getBestQuoteResolveOnlyLastCall. What happens if getBestQuoteResolveOnlyLastCall finish before the fast one? Wouldn't we update using the wrong price in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would, the solution for this is in latest commit and I tested it in a way by using setTimeout on fast price update action. And the update will check inside of update action code, if there is already a quote price amount, which means that the best quote request was already done and if the current action params are bestQuote update or not.

},
[
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
17 changes: 12 additions & 5 deletions src/custom/state/price/reducer.ts
Original file line number Diff line number Diff line change
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,7 +116,7 @@ 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
Expand All @@ -126,6 +127,11 @@ export default createReducer(initialState, (builder) =>

// 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 +153,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