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:
parent
b3d772bdb5
commit
abfd87c517
@ -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",
|
||||
|
114
src/hooks/usePoolTickData.ts
Normal file
114
src/hooks/usePoolTickData.ts
Normal file
@ -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`
|
||||
|
59
src/utils/computeSurroundingTicks.ts
Normal file
59
src/utils/computeSurroundingTicks.ts
Normal file
@ -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
|
||||
}
|
24
yarn.lock
24
yarn.lock
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user