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

Commit

Permalink
Merge pull request #2016 from gnosis/1.8.0/gp-strategy-url
Browse files Browse the repository at this point in the history
[Release 1.8] Use real price strategy endpoint
  • Loading branch information
W3stside authored Dec 22, 2021
2 parents 91ff4e9 + cb5ca4a commit 74ca480
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 57 deletions.
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

0 comments on commit 74ca480

Please sign in to comment.