feat(pools): fetch pool tick data from uniswap v3 subgraph (#1932)

* add hooks to fecth ticks data and active liquidity

* cleanup

* add polling interval

* moved ms.macro types to dev deps

* generate graphql schema on build

* added @types/ms.macro

* use clone deep
This commit is contained in:
Justin Domingue 2021-07-01 11:08:53 -07:00 committed by GitHub
parent b3d772bdb5
commit abfd87c517
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 218 additions and 2 deletions

@ -1,3 +1,4 @@
overrideExisting: true
schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
documents: 'src/**/!(*.d).{ts,tsx}'
generates:

@ -24,8 +24,10 @@
"@types/lingui__core": "^2.7.1",
"@types/lingui__macro": "^2.7.4",
"@types/lingui__react": "^2.8.3",
"@types/lodash.clonedeep": "^4.5.6",
"@types/lodash.flatmap": "^4.5.6",
"@types/luxon": "^1.24.4",
"@types/ms.macro": "^2.0.0",
"@types/multicodec": "^1.0.0",
"@types/node": "^13.13.5",
"@types/qs": "^6.9.2",
@ -74,8 +76,10 @@
"graphql-request": "^3.4.0",
"inter-ui": "^3.13.1",
"lightweight-charts": "^3.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.flatmap": "^4.5.0",
"luxon": "^1.25.0",
"ms.macro": "^2.0.0",
"multicodec": "^3.0.1",
"multihashes": "^4.0.2",
"node-vibrant": "^3.1.5",

@ -0,0 +1,114 @@
import { Currency } from '@uniswap/sdk-core'
import { FeeAmount, Pool, tickToPrice, TICK_SPACINGS } from '@uniswap/v3-sdk'
import JSBI from 'jsbi'
import { PoolState, usePool } from './usePools'
import { useEffect, useMemo, useState } from 'react'
import computeSurroundingTicks from 'utils/computeSurroundingTicks'
import { useAllV3TicksQuery } from 'state/data/generated'
import { skipToken } from '@reduxjs/toolkit/query/react'
import ms from 'ms.macro'
import cloneDeep from 'lodash/cloneDeep'
const PRICE_FIXED_DIGITS = 8
// Tick with fields parsed to JSBIs, and active liquidity computed.
export interface TickProcessed {
tickIdx: number
liquidityActive: JSBI
liquidityNet: JSBI
price0: string
}
const getActiveTick = (tickCurrent: number | undefined, feeAmount: FeeAmount | undefined) =>
tickCurrent && feeAmount ? Math.floor(tickCurrent / TICK_SPACINGS[feeAmount]) * TICK_SPACINGS[feeAmount] : undefined
// Fetches all ticks for a given pool
export function useAllV3Ticks(
currencyA: Currency | undefined,
currencyB: Currency | undefined,
feeAmount: FeeAmount | undefined
) {
const poolAddress =
currencyA && currencyB && feeAmount ? Pool.getAddress(currencyA?.wrapped, currencyB?.wrapped, feeAmount) : undefined
//TODO(judo): determine if pagination is necessary for this query
const { isLoading, isError, data } = useAllV3TicksQuery(
poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken,
{
pollingInterval: ms`2m`,
}
)
return {
isLoading,
isError,
ticks: data?.ticks,
}
}
export function usePoolActiveLiquidity(
currencyA: Currency | undefined,
currencyB: Currency | undefined,
feeAmount: FeeAmount | undefined
): {
isLoading: boolean
isError: boolean
activeTick: number | undefined
data: TickProcessed[]
} {
const [ticksProcessed, setTicksProcessed] = useState<TickProcessed[]>([])
const pool = usePool(currencyA, currencyB, feeAmount)
const { isLoading, isError, ticks } = useAllV3Ticks(currencyA, currencyB, feeAmount)
// Find nearest valid tick for pool in case tick is not initialized.
const activeTick = useMemo(() => getActiveTick(pool[1]?.tickCurrent, feeAmount), [pool, feeAmount])
useEffect(() => {
if (!currencyA || !currencyB || !activeTick || pool[0] !== PoolState.EXISTS || !ticks || ticks.length === 0) {
setTicksProcessed([])
return
}
const token0 = currencyA?.wrapped
const token1 = currencyB?.wrapped
const sortedTickData = cloneDeep(ticks)
sortedTickData.sort((a, b) => a.tickIdx - b.tickIdx)
// find where the active tick would be to partition the array
// if the active tick is initialized, the pivot will be an element
// if not, take the previous tick as pivot
const pivot = sortedTickData.findIndex(({ tickIdx }) => tickIdx > activeTick) - 1
if (pivot < 0) {
// consider setting a local error
console.error('TickData pivot not found')
return
}
const activeTickProcessed: TickProcessed = {
liquidityActive: JSBI.BigInt(pool[1]?.liquidity ?? 0),
tickIdx: activeTick,
liquidityNet:
sortedTickData[pivot].tickIdx === activeTick ? JSBI.BigInt(sortedTickData[pivot].liquidityNet) : JSBI.BigInt(0),
price0: tickToPrice(token0, token1, activeTick).toFixed(PRICE_FIXED_DIGITS),
}
const subsequentTicks = computeSurroundingTicks(token0, token1, activeTickProcessed, sortedTickData, pivot, true)
const previousTicks = computeSurroundingTicks(token0, token1, activeTickProcessed, sortedTickData, pivot, false)
const newTicksProcessed = previousTicks.concat(activeTickProcessed).concat(subsequentTicks)
setTicksProcessed(newTicksProcessed)
}, [currencyA, currencyB, activeTick, pool, ticks])
return {
isLoading: isLoading || pool[0] === PoolState.LOADING,
isError: isError || pool[0] === PoolState.INVALID,
activeTick,
data: ticksProcessed,
}
}

@ -49,6 +49,24 @@ export const api = createApi({
reducerPath: 'dataApi',
baseQuery: graphqlRequestBaseQuery(),
endpoints: (builder) => ({
getAllV3Ticks: builder.query({
query: ({ poolAddress, skip = 0 }) => ({
document: gql`
query allV3Ticks($poolAddress: String!, $skip: Int!) {
ticks(first: 1000, skip: $skip, where: { poolAddress: $poolAddress }) {
tickIdx
liquidityNet
price0
price1
}
}
`,
variables: {
poolAddress,
skip,
},
}),
}),
getFeeTierDistribution: builder.query({
query: ({ token0, token1 }) => ({
document: gql`

@ -0,0 +1,59 @@
import { Token } from '@uniswap/sdk-core'
import { tickToPrice } from '@uniswap/v3-sdk'
import { TickProcessed } from 'hooks/usePoolTickData'
import JSBI from 'jsbi'
import { AllV3TicksQuery } from 'state/data/generated'
const PRICE_FIXED_DIGITS = 8
// Computes the numSurroundingTicks above or below the active tick.
export default function computeSurroundingTicks(
token0: Token,
token1: Token,
activeTickProcessed: TickProcessed,
sortedTickData: AllV3TicksQuery['ticks'],
pivot: number,
ascending: boolean
): TickProcessed[] {
let previousTickProcessed: TickProcessed = {
...activeTickProcessed,
}
// Iterate outwards (either up or down depending on direction) from the active tick,
// building active liquidity for every tick.
let processedTicks: TickProcessed[] = []
for (let i = pivot + (ascending ? 1 : -1); ascending ? i < sortedTickData.length : i >= 0; ascending ? i++ : i--) {
const tickIdx = Number(sortedTickData[i].tickIdx)
const currentTickProcessed: TickProcessed = {
liquidityActive: previousTickProcessed.liquidityActive,
tickIdx,
liquidityNet: JSBI.BigInt(sortedTickData[i].liquidityNet),
price0: tickToPrice(token0, token1, tickIdx).toFixed(PRICE_FIXED_DIGITS),
}
// Update the active liquidity.
// If we are iterating ascending and we found an initialized tick we immediately apply
// it to the current processed tick we are building.
// If we are iterating descending, we don't want to apply the net liquidity until the following tick.
if (ascending) {
currentTickProcessed.liquidityActive = JSBI.add(
previousTickProcessed.liquidityActive,
JSBI.BigInt(sortedTickData[i].liquidityNet)
)
} else if (!ascending && JSBI.notEqual(previousTickProcessed.liquidityNet, JSBI.BigInt(0))) {
// We are iterating descending, so look at the previous tick and apply any net liquidity.
currentTickProcessed.liquidityActive = JSBI.subtract(
previousTickProcessed.liquidityActive,
previousTickProcessed.liquidityNet
)
}
processedTicks.push(currentTickProcessed)
previousTickProcessed = currentTickProcessed
}
if (!ascending) {
processedTicks = processedTicks.reverse()
}
return processedTicks
}

@ -3710,6 +3710,13 @@
"@types/lingui__core" "*"
"@types/react" "*"
"@types/lodash.clonedeep@^4.5.6":
version "4.5.6"
resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.6.tgz#3b6c40a0affe0799a2ce823b440a6cf33571d32b"
integrity sha512-cE1jYr2dEg1wBImvXlNtp0xDoS79rfEdGozQVgliDZj1uERH4k+rmEMTudP9b4VQ8O6nRb5gPqft0QzEQGMQgA==
dependencies:
"@types/lodash" "*"
"@types/lodash.flatmap@^4.5.6":
version "4.5.6"
resolved "https://registry.npmjs.org/@types/lodash.flatmap/-/lodash.flatmap-4.5.6.tgz"
@ -3732,6 +3739,11 @@
resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz"
integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==
"@types/ms.macro@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/ms.macro/-/ms.macro-2.0.0.tgz#a27e14d8e409fc973715701465e18be5edcfd4c0"
integrity sha512-xT4rTPFfZv2KZa0PLgB6e4RC5SuIr+8NDq2iZ4YxNtbByZRR98qbnS4WyNCQZIC5Jwn55AFok2CEqEb95w/CQQ==
"@types/multicodec@^1.0.0":
version "1.0.0"
resolved "https://registry.npmjs.org/@types/multicodec/-/multicodec-1.0.0.tgz"
@ -12458,7 +12470,7 @@ lodash.camelcase@^4.3.0:
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
lodash.debounce@^4.0.8:
@ -13069,6 +13081,14 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4"
run-queue "^1.0.3"
ms.macro@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms.macro/-/ms.macro-2.0.0.tgz#ff72d230cde33797d30b9b61eb57d4940dddd66f"
integrity sha512-vkb83Sa4BZ2ynF/C1x5D8ofExja36mYW6OB7JNh6Ek0NSw3Oj4moM0nN69rfbm28aHlON52E+dTEgW+3up3x1g==
dependencies:
babel-plugin-macros "^2.0.0"
ms "^2.0.0"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
@ -13084,7 +13104,7 @@ ms@2.1.2:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@^2.1.1:
ms@^2.0.0, ms@^2.1.1:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==