feat: [info] Migrate Historical Pool Data Queries (#7310)

* correctly query pool data for t24, t48, and tWeek timestamps

* add comments

* sanitize pool data and update tests

* correct test data

* add todo

* lint

* remove logs

* 1st class var support courtesy of carter

* remove logging and adds comments
This commit is contained in:
Charles Bachmeier 2023-09-22 09:47:25 -07:00 committed by GitHub
parent df6c44d2c4
commit 622c72d4a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 442 additions and 29 deletions

@ -23,6 +23,18 @@ import { ARBITRUM_LIST, AVALANCHE_LIST, BASE_LIST, CELO_LIST, OPTIMISM_LIST, PLA
export const AVERAGE_L1_BLOCK_TIME = ms(`12s`)
// The block number at which v3 was deployed on each chain, separate from the UNIVERSAL_ROUTER_CREATION_BLOCK
export const START_BLOCKS: { [key: number]: number } = {
[ChainId.MAINNET]: 14292820,
[ChainId.POLYGON]: 25459720,
[ChainId.ARBITRUM_ONE]: 175,
[ChainId.OPTIMISM]: 10028767,
[ChainId.CELO]: 13916355,
[ChainId.BNB]: 26324014,
[ChainId.AVALANCHE]: 31422450,
[ChainId.BASE]: 1371680,
}
export enum NetworkType {
L1,
L2,

@ -2,12 +2,20 @@ import { ChainId } from '@uniswap/sdk-core'
import gql from 'graphql-tag'
import { useMemo } from 'react'
import { usePoolDataQuery } from './__generated__/types-and-hooks'
import { Pool, Token, usePoolDataQuery } from './__generated__/types-and-hooks'
import { chainToApolloClient } from './apollo'
import { useBlocksFromTimestamps } from './useBlocksFromTimestamps'
import { get2DayChange, useDeltaTimestamps } from './utils'
gql`
query PoolData($poolId: [ID!]) {
data: pools(where: { id_in: $poolId }, orderBy: totalValueLockedUSD, orderDirection: desc, subgraphError: allow) {
query PoolData($poolId: [ID!], $block: Block_height = null) {
pools(
where: { id_in: $poolId }
block: $block
orderBy: totalValueLockedUSD
orderDirection: desc
subgraphError: allow
) {
id
feeTier
liquidity
@ -43,14 +51,187 @@ gql`
}
`
export function usePoolData(poolAddress: string, chainId?: ChainId) {
interface PoolData {
// basic token info
address: string
feeTier: number
token0: Token
token1: Token
// for tick math
liquidity: number
sqrtPrice: number
tick: number
// volume
volumeUSD: number
volumeUSDChange: number
volumeUSDWeek: number
// liquidity
tvlUSD: number
tvlUSDChange: number
// prices
token0Price: number
token1Price: number
// token amounts
tvlToken0: number
tvlToken1: number
}
export function usePoolData(
poolAddress: string,
chainId?: ChainId
): {
loading: boolean
error: boolean
data?: PoolData
} {
const poolId = [poolAddress]
const apolloClient = chainToApolloClient[chainId || ChainId.MAINNET]
const { data, loading } = usePoolDataQuery({ variables: { poolId }, client: apolloClient })
// get blocks from historic timestamps
const [t24, t48, tWeek] = useDeltaTimestamps()
const { blocks, error: blockError } = useBlocksFromTimestamps([t24, t48, tWeek], chainId || ChainId.MAINNET)
const [block24, block48, blockWeek] = blocks ?? []
const { loading, error, data } = usePoolDataQuery({
variables: { poolId },
client: apolloClient,
fetchPolicy: 'no-cache',
})
const {
loading: loading24,
error: error24,
data: data24,
} = usePoolDataQuery({
variables: { poolId, block: { number: parseFloat(block24?.number) } },
client: apolloClient,
fetchPolicy: 'no-cache',
})
const {
loading: loading48,
error: error48,
data: data48,
} = usePoolDataQuery({
variables: { poolId, block: { number: parseFloat(block48?.number) } },
client: apolloClient,
fetchPolicy: 'no-cache',
})
const {
loading: loadingWeek,
error: errorWeek,
data: dataWeek,
} = usePoolDataQuery({
variables: { poolId, block: { number: parseFloat(blockWeek?.number) } },
client: apolloClient,
fetchPolicy: 'no-cache',
})
return useMemo(() => {
return {
data: data?.data[0],
loading,
const anyError = Boolean(error || error24 || error48 || blockError || errorWeek)
const anyLoading = Boolean(loading || loading24 || loading48 || loadingWeek)
// return early if not all data yet
if (anyError || anyLoading) {
return {
loading: anyLoading,
error: anyError,
data: undefined,
}
}
}, [data, loading])
// format data and calculate daily changes
const current: Pool | undefined = data?.pools[0] as Pool
const oneDay: Pool | undefined = data24?.pools[0] as Pool
const twoDay: Pool | undefined = data48?.pools[0] as Pool
const week: Pool | undefined = dataWeek?.pools[0] as Pool
const ethPriceUSD = data?.bundles?.[0]?.ethPriceUSD ? parseFloat(data?.bundles?.[0]?.ethPriceUSD) : 0
const [volumeUSD, volumeUSDChange] =
current && oneDay && twoDay
? get2DayChange(current.volumeUSD, oneDay.volumeUSD, twoDay.volumeUSD)
: current
? [parseFloat(current.volumeUSD), 0]
: [0, 0]
const volumeUSDWeek =
current && week
? parseFloat(current.volumeUSD) - parseFloat(week.volumeUSD)
: current
? parseFloat(current.volumeUSD)
: 0
// Hotifx: Subtract fees from TVL to correct data while subgraph is fixed.
/**
* Note: see issue desribed here https://github.com/Uniswap/v3-subgraph/issues/74
* During subgraph deploy switch this month we lost logic to fix this accounting.
* Grafted sync pending fix now.
* Verified that this hotfix is still required as of 2023-09-13
* TODO(DAT-139): Diagnose and address subgraph issue that requires this hotfix
*/
const feePercent = current ? parseFloat(current.feeTier) / 10000 / 100 : 0
const tvlAdjust0 = current?.volumeToken0 ? (parseFloat(current.volumeToken0) * feePercent) / 2 : 0
const tvlAdjust1 = current?.volumeToken1 ? (parseFloat(current.volumeToken1) * feePercent) / 2 : 0
const tvlToken0 = current ? parseFloat(current.totalValueLockedToken0) - tvlAdjust0 : 0
const tvlToken1 = current ? parseFloat(current.totalValueLockedToken1) - tvlAdjust1 : 0
let tvlUSD = current ? parseFloat(current.totalValueLockedUSD) : 0
const tvlUSDChange =
current && oneDay
? ((parseFloat(current.totalValueLockedUSD) - parseFloat(oneDay.totalValueLockedUSD)) /
parseFloat(oneDay.totalValueLockedUSD === '0' ? '1' : oneDay.totalValueLockedUSD)) *
100
: 0
// Part of TVL fix
const tvlUpdated = current
? tvlToken0 * parseFloat(current.token0.derivedETH) * ethPriceUSD +
tvlToken1 * parseFloat(current.token1.derivedETH) * ethPriceUSD
: undefined
if (tvlUpdated) {
tvlUSD = tvlUpdated
}
return {
data: current
? {
...current,
address: poolAddress,
volumeUSD,
volumeUSDChange,
volumeUSDWeek,
tvlUSD,
tvlUSDChange,
tvlToken0,
tvlToken1,
tick: parseFloat(current.tick),
}
: undefined,
error: anyError,
loading: anyLoading,
}
}, [
blockError,
data?.bundles,
data?.pools,
data24?.pools,
data48?.pools,
dataWeek?.pools,
error,
error24,
error48,
errorWeek,
loading,
loading24,
loading48,
loadingWeek,
poolAddress,
])
}

@ -11,6 +11,18 @@ const CHAIN_SUBGRAPH_URL: Record<number, string> = {
[ChainId.CELO]: 'https://api.thegraph.com/subgraphs/name/jesse-sawa/uniswap-celo?source=uniswap',
[ChainId.BNB]: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswap-v3-bsc?source=uniswap',
[ChainId.AVALANCHE]: 'https://api.thegraph.com/subgraphs/name/lynnshaoyu/uniswap-v3-avax?source=uniswap',
[ChainId.BASE]: 'https://api.studio.thegraph.com/query/48211/uniswap-v3-base/version/latest',
}
const CHAIN_BLOCK_SUBGRAPH_URL: Record<number, string> = {
[ChainId.MAINNET]: 'https://api.thegraph.com/subgraphs/name/blocklytics/ethereum-blocks?source=uniswap',
[ChainId.ARBITRUM_ONE]: 'https://api.thegraph.com/subgraphs/name/ianlapham/arbitrum-one-blocks?source=uniswap',
[ChainId.OPTIMISM]: 'https://api.thegraph.com/subgraphs/name/ianlapham/uni-testing-subgraph?source=uniswap',
[ChainId.POLYGON]: 'https://api.thegraph.com/subgraphs/name/ianlapham/polygon-blocks?source=uniswap',
[ChainId.CELO]: 'https://api.thegraph.com/subgraphs/name/jesse-sawa/celo-blocks?source=uniswap',
[ChainId.BNB]: 'https://api.thegraph.com/subgraphs/name/wombat-exchange/bnb-chain-block?source=uniswap',
[ChainId.AVALANCHE]: 'https://api.thegraph.com/subgraphs/name/lynnshaoyu/avalanche-blocks?source=uniswap',
[ChainId.BASE]: 'https://api.studio.thegraph.com/query/48211/base-blocks/version/latest?source=uniswap',
}
const httpLink = new HttpLink({ uri: CHAIN_SUBGRAPH_URL[ChainId.MAINNET] })
@ -63,3 +75,34 @@ export const chainToApolloClient: Record<number, ApolloClient<NormalizedCacheObj
uri: CHAIN_SUBGRAPH_URL[ChainId.AVALANCHE],
}),
}
export const chainToApolloBlockClient: Record<number, ApolloClient<NormalizedCacheObject>> = {
[ChainId.MAINNET]: new ApolloClient({
uri: CHAIN_BLOCK_SUBGRAPH_URL[ChainId.MAINNET],
cache: new InMemoryCache(),
}),
[ChainId.ARBITRUM_ONE]: new ApolloClient({
uri: CHAIN_BLOCK_SUBGRAPH_URL[ChainId.ARBITRUM_ONE],
cache: new InMemoryCache(),
}),
[ChainId.OPTIMISM]: new ApolloClient({
uri: CHAIN_BLOCK_SUBGRAPH_URL[ChainId.OPTIMISM],
cache: new InMemoryCache(),
}),
[ChainId.POLYGON]: new ApolloClient({
uri: CHAIN_BLOCK_SUBGRAPH_URL[ChainId.POLYGON],
cache: new InMemoryCache(),
}),
[ChainId.CELO]: new ApolloClient({
uri: CHAIN_BLOCK_SUBGRAPH_URL[ChainId.CELO],
cache: new InMemoryCache(),
}),
[ChainId.BNB]: new ApolloClient({
uri: CHAIN_BLOCK_SUBGRAPH_URL[ChainId.BNB],
cache: new InMemoryCache(),
}),
[ChainId.AVALANCHE]: new ApolloClient({
uri: CHAIN_BLOCK_SUBGRAPH_URL[ChainId.AVALANCHE],
cache: new InMemoryCache(),
}),
}

@ -0,0 +1,83 @@
import { ChainId } from '@uniswap/sdk-core'
import { START_BLOCKS } from 'constants/chainInfo'
import gql from 'graphql-tag'
import { useEffect, useMemo, useState } from 'react'
import { chainToApolloBlockClient } from './apollo'
import { splitQuery } from './utils'
const GET_BLOCKS = (timestamps: string[]) => {
let queryString = 'query blocks {'
queryString += timestamps.map((timestamp) => {
return `t${timestamp}:blocks(first: 1, orderBy: timestamp, orderDirection: desc, where: { timestamp_gt: ${timestamp}, timestamp_lt: ${
timestamp + 600
} }) {
number
}`
})
queryString += '}'
return gql(queryString)
}
/**
* for a given array of timestamps, returns block entities
* @param timestamps
*/
export function useBlocksFromTimestamps(
timestamps: number[],
chainId: ChainId
): {
blocks?: {
timestamp: string
number: any
}[]
error: boolean
} {
const [blocks, setBlocks] = useState<any>()
const [error, setError] = useState(false)
const chainBlockClient = chainToApolloBlockClient[chainId]
// derive blocks based on active network
const networkBlocks = blocks?.[chainId]
useEffect(() => {
async function fetchData() {
const results = await splitQuery(GET_BLOCKS, chainBlockClient, [], timestamps)
if (results) {
setBlocks({ ...(blocks ?? {}), [chainId]: results })
} else {
setError(true)
}
}
if (!networkBlocks && !error) {
fetchData()
}
})
const blocksFormatted = useMemo(() => {
if (blocks?.[chainId]) {
const networkBlocks = blocks?.[chainId]
const formatted = []
for (const t in networkBlocks) {
if (networkBlocks[t].length > 0) {
const number = networkBlocks[t][0]['number']
const deploymentBlock = START_BLOCKS[chainId]
const adjustedNumber = number > deploymentBlock ? number : deploymentBlock
formatted.push({
timestamp: t.split('t')[1],
number: adjustedNumber,
})
}
}
return formatted
}
return undefined
}, [chainId, blocks])
return {
blocks: blocksFormatted,
error,
}
}

@ -0,0 +1,79 @@
import { ApolloClient, NormalizedCacheObject } from '@apollo/client'
import ms from 'ms'
// TODO(WEB-2878): See if splitQuery can be replaced with proper Apollo usage
/**
* Used to get large amounts of data when larger than the Apollo limit
* Splits query into multiple queries and returns the combined data
* @param query - the query to be split
* @param localClient - Apollo client for a specific chain
* @param vars - any variables that are passed in every query
* @param values - the keys that are used as the values to map over if
* @param skipCount - amount of entities to skip per query
*/
export async function splitQuery<Type extends object>(
query: any,
client: ApolloClient<NormalizedCacheObject>,
vars: any[],
values: any[],
skipCount = 1000
) {
let fetchedData = {}
let allFound = false
let skip = 0
try {
while (!allFound) {
let end = values.length
if (skip + skipCount < values.length) {
end = skip + skipCount
}
const sliced = values.slice(skip, end)
const result = await client.query<Type>({
query: query(...vars, sliced),
fetchPolicy: 'network-only',
})
fetchedData = {
...fetchedData,
...result.data,
}
if (Object.keys(result.data).length < skipCount || skip + skipCount > values.length) {
allFound = true
} else {
skip += skipCount
}
}
return fetchedData
} catch (e) {
console.log(e)
return undefined
}
}
/**
* Get the 24hr, 48hr, and 1 week ago timestamps
* @returns [t24, t48, tWeek]
*/
export function useDeltaTimestamps(): [number, number, number] {
const utcCurrentTime = Date.now()
const t24 = Math.floor((utcCurrentTime - ms('1d')) / 1000)
const t48 = Math.floor((utcCurrentTime - ms('2d')) / 1000)
const tWeek = Math.floor((utcCurrentTime - ms('7d')) / 1000)
return [t24, t48, tWeek]
}
/**
* gets the amount difference plus the % change in change itself (second order change)
* @param {*} valueNow
* @param {*} value24HoursAgo
* @param {*} value48HoursAgo
*/
export const get2DayChange = (valueNow: string, value24HoursAgo: string, value48HoursAgo: string): [number, number] => {
// get volume info for both 24 hour periods
const currentChange = parseFloat(valueNow) - parseFloat(value24HoursAgo)
const previousChange = parseFloat(value24HoursAgo) - parseFloat(value48HoursAgo)
const adjustedPercentChange = ((currentChange - previousChange) / previousChange) * 100
if (isNaN(adjustedPercentChange) || !isFinite(adjustedPercentChange)) {
return [currentChange, 0]
}
return [currentChange, adjustedPercentChange]
}

@ -67,6 +67,7 @@ describe('PoolDetailsPage', () => {
mocked(usePoolData).mockReturnValue({
data: undefined,
loading: false,
error: false,
})
render(<PoolDetails />)
@ -77,7 +78,11 @@ describe('PoolDetailsPage', () => {
// TODO replace with loading skeleton when designed
it('nothing displayed while data is loading', () => {
mocked(usePoolData).mockReturnValue({ data: undefined, loading: true })
mocked(usePoolData).mockReturnValue({
data: undefined,
loading: true,
error: false,
})
render(<PoolDetails />)
waitFor(() => {

@ -1,37 +1,47 @@
import { Token } from 'graphql/thegraph/__generated__/types-and-hooks'
export const validParams = { poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640', chainName: 'ethereum' }
export const validPoolDataResponse = {
data: {
__typename: 'Pool' as const,
id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
feeTier: '500',
liquidity: '32118065613640312417',
sqrtPrice: '1943494374075311739809880994923792',
tick: '202163',
feeTier: 500,
liquidity: parseFloat('26414803986874770777'),
sqrtPrice: parseFloat('1977320351696380862605029898750440'),
tick: 202508,
token0: {
__typename: 'Token' as const,
id: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
symbol: 'USDC',
name: 'USD Coin',
decimals: '6',
derivedETH: '0.000602062055419695968472438533210813',
},
derivedETH: '0.0006240873011635544626425964678706127',
__typename: 'Token',
} as Token,
token1: {
__typename: 'Token' as const,
id: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
symbol: 'WETH',
name: 'Wrapped Ether',
decimals: '18',
derivedETH: '1',
},
token0Price: '1661.85294822715829371652214854595',
token1Price: '0.0006017379582632664031212782038199158',
volumeUSD: '394920157156.0515346899898790592366',
volumeToken0: '394894081779.781168',
volumeToken1: '190965971.266407832255075308',
txCount: '5406827',
totalValueLockedToken0: '180078648.881221',
totalValueLockedToken1: '142782.017882048454494774',
totalValueLockedUSD: '417233634.1468435997761171520463339',
__typename: 'Token',
} as Token,
token0Price: 1605.481,
token1Price: 0.000622,
volumeUSD: 233379442.64648438,
volumeToken0: '397309311915.656392',
volumeToken1: '192461624.767400825529358443',
txCount: '5456494',
totalValueLockedToken0: '190258041.714605',
totalValueLockedToken1: '130641.89297715763283183',
totalValueLockedUSD: '399590762.8476702153638342035105795',
__typename: 'Pool',
address: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
volumeUSDChange: -17.753809465717136,
volumeUSDWeek: 1359911419.265625,
tvlUSD: 223166198.4690675,
tvlUSDChange: -0.3657085465786977,
tvlToken0: 90930713.7356909,
tvlToken1: 82526.48678530742,
},
loading: false,
error: false,
}