Skip to content

Commit

Permalink
feat(pools): fetch pool tick data from uniswap v3 subgraph (Uniswap#1932
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
Justin Domingue authored Jul 1, 2021
1 parent b3d772b commit abfd87c
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 2 deletions.
1 change: 1 addition & 0 deletions codegen.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
overrideExisting: true
schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
documents: 'src/**/!(*.d).{ts,tsx}'
generates:
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
114 changes: 114 additions & 0 deletions src/hooks/usePoolTickData.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
18 changes: 18 additions & 0 deletions src/state/data/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
59 changes: 59 additions & 0 deletions src/utils/computeSurroundingTicks.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 22 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"

[email protected]:
version "2.0.0"
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
Expand All @@ -13084,7 +13104,7 @@ [email protected]:
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==
Expand Down

0 comments on commit abfd87c

Please sign in to comment.