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

[Release 1.8] Use real price strategy endpoint #2016

Merged
merged 6 commits into from
Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
50 changes: 49 additions & 1 deletion src/custom/api/gnosisProtocol/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SupportedChainId as ChainId } from 'constants/chains'
import { SupportedChainId as ChainId, SupportedChainId } from 'constants/chains'
import { OrderKind, QuoteQuery } from '@gnosis.pm/gp-v2-contracts'
import { stringify } from 'qs'
import { getSigningSchemeApiValue, OrderCreation, OrderCancellation, SigningSchemeValue } from 'utils/signatures'
Expand All @@ -24,6 +24,7 @@ import { GAS_FEE_ENDPOINTS } from 'constants/index'
import * as Sentry from '@sentry/browser'
import { ZERO_ADDRESS } from 'constants/misc'
import { getAppDataHash } from 'constants/appDataHash'
import { GpPriceStrategy } from 'hooks/useGetGpPriceStrategy'

function getGnosisProtocolUrl(): Partial<Record<ChainId, string>> {
if (isLocal || isDev || isPr || isBarn) {
Expand Down Expand Up @@ -58,9 +59,18 @@ function getProfileUrl(): Partial<Record<ChainId, string>> {
process.env.REACT_APP_PROFILE_API_URL_STAGING_MAINNET || 'https://protocol-affiliate.gnosis.io/api',
}
}
const STRATEGY_URL_BASE = 'https://raw.githubusercontent.com/gnosis/cowswap/configuration/config/strategies'
function getPriceStrategyUrl(): Record<SupportedChainId, string> {
return {
[SupportedChainId.MAINNET]: STRATEGY_URL_BASE + '/strategy-1.json',
[SupportedChainId.RINKEBY]: STRATEGY_URL_BASE + '/strategy-4.json',
[SupportedChainId.XDAI]: STRATEGY_URL_BASE + '/strategy-100.json',
}
}

const API_BASE_URL = getGnosisProtocolUrl()
const PROFILE_API_BASE_URL = getProfileUrl()
const STRATEGY_API_URL = getPriceStrategyUrl()

const DEFAULT_HEADERS = {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -145,6 +155,20 @@ function _getProfileApiBaseUrl(chainId: ChainId): string {
}
}

function _getPriceStrategyApiBaseUrl(chainId: ChainId): string {
const baseUrl = STRATEGY_API_URL[chainId]

if (!baseUrl) {
new Error(
`Unsupported Network. The ${API_NAME} strategy API is not deployed in the Network ` +
chainId +
'. Defaulting to using Mainnet strategy.'
)
}

return baseUrl
}

export function getOrderLink(chainId: ChainId, orderId: OrderID): string {
const baseUrl = _getApiBaseUrl(chainId)

Expand Down Expand Up @@ -174,6 +198,11 @@ function _fetchProfile(
})
}

function _fetchPriceStrategy(chainId: ChainId): Promise<Response> {
const baseUrl = _getPriceStrategyApiBaseUrl(chainId)
return fetch(baseUrl)
}

function _post(chainId: ChainId, url: string, data: any): Promise<Response> {
return _fetch(chainId, url, 'POST', data)
}
Expand Down Expand Up @@ -427,6 +456,25 @@ export async function getProfileData(chainId: ChainId, address: string): Promise
}
}

export type PriceStrategy = {
primary: GpPriceStrategy
secondary: GpPriceStrategy
}

export async function getPriceStrategy(chainId: ChainId): Promise<PriceStrategy> {
console.log(`[api:${API_NAME}] Get GP price strategy for`, chainId)

const response = await _fetchPriceStrategy(chainId)

if (!response.ok) {
const errorResponse = await response.json()
console.log(errorResponse)
throw new Error(errorResponse?.description)
} else {
return response.json()
}
}

export interface GasFeeEndpointResponse {
lastUpdate: string
lowest: string
Expand Down
2 changes: 1 addition & 1 deletion src/custom/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,4 @@ export const STORAGE_KEY_LAST_PROVIDER = 'lastProvider'
// Default price strategy to use for getting app prices
// COWSWAP = new quote endpoint
// LEGACY = price racing logic (checking 0x, gp, paraswap, etc)
export const DEFAULT_GP_PRICE_STRATEGY = 'LEGACY'
export const DEFAULT_GP_PRICE_STRATEGY = 'COWSWAP'
5 changes: 3 additions & 2 deletions src/custom/hooks/useDebounceWithForceUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import useDebounce from '@src/hooks/useDebounce'
import { useEffect, useState } from 'react'

// modified from https://usehooks.com/useDebounce/
export default function useDebounceWithForceUpdate<T>(latestValue: T, delay: number, forceUpdateRef?: any): T {
export default function useDebounceWithForceUpdate<T>(latestValue: T, delay: number, forceUpdateRef: any[]): T {
// const value = useRef(latestValue)
const [value, setValue] = useState(latestValue)
const [needToUpdate, setNeedToUpdate] = useState(false)

// Force update
useEffect(() => setNeedToUpdate(true), [forceUpdateRef])
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => setNeedToUpdate(true), [...forceUpdateRef])
useEffect(() => {
if (needToUpdate) {
setNeedToUpdate(false)
Expand Down
33 changes: 15 additions & 18 deletions src/custom/hooks/useGetGpPriceStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import ms from 'ms.macro'
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { DEFAULT_GP_PRICE_STRATEGY } from 'constants/index'
import { registerOnWindow } from '../utils/misc'
import { getPriceStrategy, PriceStrategy } from 'api/gnosisProtocol/api'
import { useActiveWeb3React } from 'hooks'
import { supportedChainId } from 'utils/supportedChainId'
import { SupportedChainId } from 'constants/chains'

export type GpPriceStrategy = 'COWSWAP' | 'LEGACY'
// TODO: use actual API call
// https://github.com/gnosis/gp-v2-contracts/issues/904
export async function checkGpPriceStrategy(): Promise<GpPriceStrategy> {
return new Promise((accept) => setTimeout(() => accept(DEFAULT_GP_PRICE_STRATEGY), 500))
}

// arbitrary, could be more/less
const GP_PRICE_STRATEGY_INTERVAL_TIME = ms`30 minutes`

export default function useGetGpPriceStrategy(): GpPriceStrategy {
const [gpPriceStrategy, setGpPriceStrategy] = useState<GpPriceStrategy>(DEFAULT_GP_PRICE_STRATEGY)
const { chainId: preChainId } = useActiveWeb3React()

const _handleSetStrategy = useCallback((response: PriceStrategy) => setGpPriceStrategy(response.primary), [])

useEffect(() => {
const chainId = supportedChainId(preChainId)
console.debug('[useGetGpPriceStrategy::GP Price Strategy]::', gpPriceStrategy)

const getStrategy = () => {
checkGpPriceStrategy()
.then(setGpPriceStrategy)
// default to MAINNET if not connected, or incorrect network
getPriceStrategy(chainId || SupportedChainId.MAINNET)
.then(_handleSetStrategy)
.catch((err: Error) => {
console.error('[useGetGpPriceStrategy::useEffect] Error getting GP price strategy::', err)
// Fallback to DEFAULT
Expand All @@ -36,14 +40,7 @@ export default function useGetGpPriceStrategy(): GpPriceStrategy {
}, GP_PRICE_STRATEGY_INTERVAL_TIME)

return () => clearInterval(intervalId)
}, [gpPriceStrategy])
}, [_handleSetStrategy, gpPriceStrategy, preChainId])

// TODO: REMOVE
return process.env.NODE_ENV !== 'production' ? (window as any).GP_STRATEGY : gpPriceStrategy
return gpPriceStrategy
}

/* TESTING ONLY! */
;(window as any).GP_STRATEGY = DEFAULT_GP_PRICE_STRATEGY
registerOnWindow({
setStrategy: (strat: GpPriceStrategy) => ((window as any).GP_STRATEGY = strat),
})
14 changes: 11 additions & 3 deletions src/custom/hooks/usePriceImpact/useFallbackPriceImpact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { QuoteInformationObject } from 'state/price/reducer'
import { QuoteError } from 'state/price/actions'
import { useQuote } from 'state/price/hooks'
import { useActiveWeb3React } from 'hooks/web3'
import { TYPED_VALUE_DEBOUNCE_TIME } from 'state/price/updater'
import useDebounceWithForceUpdate from 'hooks/useDebounceWithForceUpdate'

type SwapParams = { abTrade?: TradeGp; sellToken?: string | null; buyToken?: string | null }

Expand Down Expand Up @@ -41,13 +43,19 @@ function _getBaTradeParsedAmount(abTrade: TradeGp | undefined, shouldCalculate:
return abTrade?.outputAmountWithoutFee
}

export default function useFallbackPriceImpact({ abTrade, isWrapping }: FallbackPriceImpactParams) {
export default function useFallbackPriceImpact({ abTrade: rawAbTrade, isWrapping }: FallbackPriceImpactParams) {
const {
typedValue,
INPUT: { currencyId: sellToken },
OUTPUT: { currencyId: buyToken },
} = useSwapState()

// debounce trade creation and force update when tradeType changes
const abTrade = useDebounceWithForceUpdate(rawAbTrade, TYPED_VALUE_DEBOUNCE_TIME, [
rawAbTrade?.tradeType,
sellToken,
buyToken,
])

const { chainId } = useActiveWeb3React()
const lastQuote = useQuote({ token: sellToken, chainId })

Expand Down Expand Up @@ -116,7 +124,7 @@ export default function useFallbackPriceImpact({ abTrade, isWrapping }: Fallback
setImpact(undefined)
setError(undefined)
}
}, [abIn, abOut, baOut, quoteError, loading, typedValue])
}, [abIn, abOut, baOut, quoteError, loading])

return { impact, error, loading }
}
10 changes: 6 additions & 4 deletions src/custom/hooks/usePriceImpact/useQuoteAndSwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import { useActiveWeb3React } from 'hooks/web3'

import { getPromiseFulfilledValue, isPromiseFulfilled } from 'utils/misc'
import { supportedChainId } from 'utils/supportedChainId'
import { FeeQuoteParams, QuoteResult } from 'utils/price'
import { FeeQuoteParams, getBestQuote, QuoteResult } from 'utils/price'

import { ZERO_ADDRESS } from 'constants/misc'
import { SupportedChainId } from 'constants/chains'
import { DEFAULT_DECIMALS } from 'constants/index'
import { QuoteError } from 'state/price/actions'
import { isWrappingTrade } from 'state/swap/utils'
import useGetGpPriceStrategy from '../useGetGpPriceStrategy'
import { getBestQuoteResolveOnlyLastCall as getBestQuote } from 'hooks/useRefetchPriceCallback'
import useGetGpPriceStrategy from 'hooks/useGetGpPriceStrategy'
import { onlyResolvesLast } from 'utils/async'

type WithLoading = { loading: boolean; setLoading: (state: boolean) => void }

Expand All @@ -39,6 +39,8 @@ type GetQuoteParams = {

type FeeQuoteParamsWithError = FeeQuoteParams & { error?: QuoteError }

const getBestQuoteResolveOnlyLastCall = onlyResolvesLast<QuoteResult>(getBestQuote)

export function useCalculateQuote(params: GetQuoteParams) {
const {
amountAtoms: amount,
Expand Down Expand Up @@ -77,7 +79,7 @@ export function useCalculateQuote(params: GetQuoteParams) {
validTo,
}
let quoteData: QuoteInformationObject | FeeQuoteParams = quoteParams
getBestQuote({
getBestQuoteResolveOnlyLastCall({
strategy,
quoteParams,
fetchFee: true,
Expand Down
6 changes: 5 additions & 1 deletion src/custom/hooks/useRefetchPriceCallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function handleQuoteError({ quoteData, error, addUnsupportedToken }: Hand
}
}

export const getBestQuoteResolveOnlyLastCall = onlyResolvesLast<QuoteResult>(getBestQuote)
const getBestQuoteResolveOnlyLastCall = onlyResolvesLast<QuoteResult>(getBestQuote)

/**
* @returns callback that fetches a new quote and update the state
Expand Down Expand Up @@ -151,6 +151,10 @@ export function useRefetchQuoteCallback() {
getNewQuote(quoteParams)
}

registerOnWindow({
getBestQuote: async () => getBestQuoteResolveOnlyLastCall({ ...params, strategy: priceStrategy }),
})

// Get the quote
// price can be null if fee > price
const { cancelled, data } = await getBestQuoteResolveOnlyLastCall({ ...params, strategy: priceStrategy })
Expand Down
2 changes: 1 addition & 1 deletion src/custom/hooks/useSwapCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { usePresignOrder, PresignOrder } from 'hooks/usePresignOrder'
import { Web3Provider } from '@ethersproject/providers'
import { useAppDataHash } from 'state/affiliate/hooks'

const MAX_VALID_TO_EPOCH = BigNumber.from('0xFFFFFFFF').toNumber() // Max uint32 (Feb 07 2106 07:28:15 GMT+0100)
export const MAX_VALID_TO_EPOCH = BigNumber.from('0xFFFFFFFF').toNumber() // Max uint32 (Feb 07 2106 07:28:15 GMT+0100)

export function calculateValidTo(deadline: number): number {
// Need the timestamp in seconds
Expand Down
60 changes: 37 additions & 23 deletions src/custom/hooks/useUSDCPrice/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useBestV3TradeExactOut } from './useBestV3Trade' */
import { useActiveWeb3React } from 'hooks/web3'

import { supportedChainId } from 'utils/supportedChainId'
import { getBestPrice } from 'utils/price'
import { STABLECOIN_AMOUNT_OUT as STABLECOIN_AMOUNT_OUT_UNI } from 'hooks/useUSDCPrice'
import { stringToCurrency } from 'state/swap/extension'
import { USDC_XDAI } from 'utils/xdai/constants'
Expand All @@ -18,15 +17,22 @@ import { tryParseAmount } from 'state/swap/hooks'
import { DEFAULT_NETWORK_FOR_LISTS } from 'constants/lists'
import { currencyId } from 'utils/currencyId'
import { USDC } from 'constants/tokens'
import { useOrderValidTo } from 'state/user/hooks'
import { useBlockNumber } from 'state/application/hooks'
import useGetGpPriceStrategy from 'hooks/useGetGpPriceStrategy'
import { MAX_VALID_TO_EPOCH } from 'hooks/useSwapCallback'
import { useIsQuoteLoading } from 'state/price/hooks'
import { onlyResolvesLast } from 'utils/async'
import { getGpUsdcPrice } from 'utils/price'

export * from '@src/hooks/useUSDCPrice'

const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount<Token> } = {
const getGpUsdcPriceResolveOnlyLastCall = onlyResolvesLast(getGpUsdcPrice)

const STABLECOIN_AMOUNT_OUT: { [chain in SupportedChainId]: CurrencyAmount<Token> } = {
...STABLECOIN_AMOUNT_OUT_UNI,
// MOD: lowers threshold from 100k to 100
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(USDC, 100e6),
[SupportedChainId.RINKEBY]: CurrencyAmount.fromRawAmount(USDC, 100e6),
[SupportedChainId.XDAI]: CurrencyAmount.fromRawAmount(USDC_XDAI, 10_000e6),
}

Expand All @@ -39,11 +45,12 @@ export default function useUSDCPrice(currency?: Currency) {
const [error, setError] = useState<Error | null>(null)

const { chainId, account } = useActiveWeb3React()
const { validTo } = useOrderValidTo()
const blockNumber = useBlockNumber()
// use quote loading as a price update dependency
const isQuoteLoading = useIsQuoteLoading()
const strategy = useGetGpPriceStrategy()

const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined
const stablecoin = amountOut?.currency
const sellTokenAddress = currency?.wrapped.address
const sellTokenDecimals = currency?.wrapped.decimals

/*
const v2USDCTrade = useV2TradeExactOut(currency, amountOut, {
Expand Down Expand Up @@ -75,39 +82,46 @@ export default function useUSDCPrice(currency?: Currency) {
*/

useEffect(() => {
const isSupportedChain = supportedChainId(chainId)
if (!isSupportedChain || !currency || !amountOut || !stablecoin) return
const supportedChain = supportedChainId(chainId)
if (!isQuoteLoading || !supportedChain || !sellTokenAddress || !sellTokenDecimals) return

const params = {
baseToken: stablecoin.address,
quoteToken: currency.wrapped.address,
const baseAmount = STABLECOIN_AMOUNT_OUT[supportedChain]
const stablecoin = baseAmount.currency

const quoteParams = {
buyToken: stablecoin.address,
sellToken: sellTokenAddress,
kind: OrderKind.BUY,
amount: amountOut.quotient.toString(),
chainId: isSupportedChain,
fromDecimals: currency.decimals,
amount: baseAmount.quotient.toString(),
chainId: supportedChain,
fromDecimals: sellTokenDecimals,
toDecimals: stablecoin.decimals,
userAddress: account,
validTo,
// we dont care about validTo here, just use max
validTo: MAX_VALID_TO_EPOCH,
}

if (currency.wrapped.equals(stablecoin)) {
// tokens are the same, it's 1:1
if (sellTokenAddress === stablecoin.address) {
const price = new Price(stablecoin, stablecoin, '1', '1')
return setBestUsdPrice(price)
} else {
getBestPrice(params)
.then((winningPrice) => {
getGpUsdcPriceResolveOnlyLastCall({ strategy, quoteParams })
.then(({ cancelled, data: quote }) => {
if (cancelled) return

// reset the error
setError(null)

let price: Price<Token, Currency> | null
// Response can include a null price amount
// e.g fee > input error
if (!winningPrice.amount) {
if (!quote) {
price = null
} else {
price = new Price({
baseAmount: amountOut,
quoteAmount: stringToCurrency(winningPrice.amount, currency),
baseAmount,
quoteAmount: stringToCurrency(quote, stablecoin),
})
console.debug(
'[useBestUSDCPrice] Best USDC price amount',
Expand All @@ -126,7 +140,7 @@ export default function useUSDCPrice(currency?: Currency) {
})
})
}
}, [amountOut, chainId, currency, stablecoin, account, validTo, blockNumber])
}, [account, isQuoteLoading, chainId, strategy, sellTokenAddress, sellTokenDecimals])

return { price: bestUsdPrice, error }
}
Expand Down
Loading