From 622c72d4a873783ba02ebb8d4d47458669b3b5fd Mon Sep 17 00:00:00 2001 From: Charles Bachmeier Date: Fri, 22 Sep 2023 09:47:25 -0700 Subject: [PATCH] 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 --- src/constants/chainInfo.ts | 12 ++ src/graphql/thegraph/PoolData.ts | 199 +++++++++++++++++- src/graphql/thegraph/apollo.ts | 43 ++++ .../thegraph/useBlocksFromTimestamps.ts | 83 ++++++++ src/graphql/thegraph/utils.ts | 79 +++++++ src/pages/PoolDetails/index.test.tsx | 7 +- src/test-utils/pools/fixtures.ts | 48 +++-- 7 files changed, 442 insertions(+), 29 deletions(-) create mode 100644 src/graphql/thegraph/useBlocksFromTimestamps.ts create mode 100644 src/graphql/thegraph/utils.ts diff --git a/src/constants/chainInfo.ts b/src/constants/chainInfo.ts index 29cd6d5fce..9fe2a175e5 100644 --- a/src/constants/chainInfo.ts +++ b/src/constants/chainInfo.ts @@ -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, diff --git a/src/graphql/thegraph/PoolData.ts b/src/graphql/thegraph/PoolData.ts index e0f681a7bb..d736d6e26c 100644 --- a/src/graphql/thegraph/PoolData.ts +++ b/src/graphql/thegraph/PoolData.ts @@ -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, + ]) } diff --git a/src/graphql/thegraph/apollo.ts b/src/graphql/thegraph/apollo.ts index 4938017c6d..5d95f49801 100644 --- a/src/graphql/thegraph/apollo.ts +++ b/src/graphql/thegraph/apollo.ts @@ -11,6 +11,18 @@ const CHAIN_SUBGRAPH_URL: Record = { [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 = { + [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> = { + [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(), + }), +} diff --git a/src/graphql/thegraph/useBlocksFromTimestamps.ts b/src/graphql/thegraph/useBlocksFromTimestamps.ts new file mode 100644 index 0000000000..c01d13e79e --- /dev/null +++ b/src/graphql/thegraph/useBlocksFromTimestamps.ts @@ -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() + 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, + } +} diff --git a/src/graphql/thegraph/utils.ts b/src/graphql/thegraph/utils.ts new file mode 100644 index 0000000000..08c3666b52 --- /dev/null +++ b/src/graphql/thegraph/utils.ts @@ -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( + query: any, + client: ApolloClient, + 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({ + 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] +} diff --git a/src/pages/PoolDetails/index.test.tsx b/src/pages/PoolDetails/index.test.tsx index 235b5969d6..ce6b343c02 100644 --- a/src/pages/PoolDetails/index.test.tsx +++ b/src/pages/PoolDetails/index.test.tsx @@ -67,6 +67,7 @@ describe('PoolDetailsPage', () => { mocked(usePoolData).mockReturnValue({ data: undefined, loading: false, + error: false, }) render() @@ -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() waitFor(() => { diff --git a/src/test-utils/pools/fixtures.ts b/src/test-utils/pools/fixtures.ts index ad59d4ddf7..d4815873dc 100644 --- a/src/test-utils/pools/fixtures.ts +++ b/src/test-utils/pools/fixtures.ts @@ -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, }