feat: use TickLens on chains where subgraph is not functional (#3149)

This commit is contained in:
Justin Domingue 2022-01-24 09:12:07 -05:00 committed by GitHub
parent b878d764e5
commit c9642c6cd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 184 additions and 59 deletions

@ -1,16 +1,10 @@
import { Currency } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { usePoolActiveLiquidity } from 'hooks/usePoolTickData'
import JSBI from 'jsbi'
import { TickProcessed, usePoolActiveLiquidity } from 'hooks/usePoolTickData'
import { useCallback, useMemo } from 'react'
import { ChartEntry } from './types'
export interface TickProcessed {
liquidityActive: JSBI
price0: string
}
export function useDensityChartData({
currencyA,
currencyB,

@ -107,3 +107,8 @@ export const V3_MIGRATOR_ADDRESSES: AddressMap = constructSameAddressMap('0xA564
SupportedChainId.POLYGON_MUMBAI,
SupportedChainId.POLYGON,
])
export const TICK_LENS_ADDRESSES: AddressMap = {
[SupportedChainId.ARBITRUM_ONE]: '0xbfd8137f7d1516D3ea5cA83523914859ec47F573',
[SupportedChainId.ARBITRUM_RINKEBY]: '0xbfd8137f7d1516D3ea5cA83523914859ec47F573',
}

@ -6,6 +6,7 @@ import { abi as MERKLE_DISTRIBUTOR_ABI } from '@uniswap/merkle-distributor/build
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
import { abi as QuoterABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json'
import { abi as TickLensABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/TickLens.sol/TickLens.json'
import { abi as MulticallABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json'
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import { abi as V2MigratorABI } from '@uniswap/v3-periphery/artifacts/contracts/V3Migrator.sol/V3Migrator.json'
@ -30,13 +31,14 @@ import {
MULTICALL_ADDRESS,
NONFUNGIBLE_POSITION_MANAGER_ADDRESSES,
QUOTER_ADDRESSES,
TICK_LENS_ADDRESSES,
V2_ROUTER_ADDRESS,
V3_MIGRATOR_ADDRESSES,
} from 'constants/addresses'
import { UNI, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useMemo } from 'react'
import { NonfungiblePositionManager, Quoter, UniswapInterfaceMulticall } from 'types/v3'
import { NonfungiblePositionManager, Quoter, TickLens, UniswapInterfaceMulticall } from 'types/v3'
import { V3Migrator } from 'types/v3/V3Migrator'
import { getContract } from '../utils'
@ -159,3 +161,9 @@ export function useV3NFTPositionManagerContract(withSignerIfPossible?: boolean):
export function useV3Quoter() {
return useContract<Quoter>(QUOTER_ADDRESSES, QuoterABI)
}
export function useTickLens(): TickLens | null {
const { chainId } = useActiveWeb3React()
const address = chainId ? TICK_LENS_ADDRESSES[chainId] : undefined
return useContract(address, TickLensABI) as TickLens | null
}

@ -1,30 +1,136 @@
import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency } from '@uniswap/sdk-core'
import { FeeAmount, Pool, TICK_SPACINGS, tickToPrice } from '@uniswap/v3-sdk'
import { ChainId } from '@uniswap/smart-order-router'
import { FeeAmount, nearestUsableTick, Pool, TICK_SPACINGS, tickToPrice } from '@uniswap/v3-sdk'
import { ZERO_ADDRESS } from 'constants/misc'
import JSBI from 'jsbi'
import { useSingleContractMultipleData } from 'lib/hooks/multicall'
import ms from 'ms.macro'
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useAllV3TicksQuery } from 'state/data/enhanced'
import { AllV3TicksQuery } from 'state/data/generated'
import computeSurroundingTicks from 'utils/computeSurroundingTicks'
import { useTickLens } from './useContract'
import { PoolState, usePool } from './usePools'
const PRICE_FIXED_DIGITS = 8
const CHAIN_IDS_MISSING_SUBGRAPH_DATA = [ChainId.ARBITRUM_ONE, ChainId.ARBITRUM_RINKEBY]
export interface TickData {
tick: number
liquidityNet: JSBI
liquidityGross: JSBI
}
// Tick with fields parsed to JSBIs, and active liquidity computed.
export interface TickProcessed {
tickIdx: number
tick: number
liquidityActive: JSBI
liquidityNet: JSBI
price0: string
}
const REFRESH_FREQUENCY = { blocksPerFetch: 2 }
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(
const bitmapIndex = (tick: number, tickSpacing: number) => {
return Math.floor(tick / tickSpacing / 256)
}
function useTicksFromTickLens(
currencyA: Currency | undefined,
currencyB: Currency | undefined,
feeAmount: FeeAmount | undefined,
numSurroundingTicks: number | undefined = 125
) {
const [tickDataLatestSynced, setTickDataLatestSynced] = useState<TickData[]>([])
const [poolState, pool] = usePool(currencyA, currencyB, feeAmount)
const tickSpacing = feeAmount && TICK_SPACINGS[feeAmount]
// Find nearest valid tick for pool in case tick is not initialized.
const activeTick = pool?.tickCurrent && tickSpacing ? nearestUsableTick(pool?.tickCurrent, tickSpacing) : undefined
const poolAddress =
currencyA && currencyB && feeAmount && poolState === PoolState.EXISTS
? Pool.getAddress(currencyA?.wrapped, currencyB?.wrapped, feeAmount)
: undefined
// it is also possible to grab all tick data but it is extremely slow
// bitmapIndex(nearestUsableTick(TickMath.MIN_TICK, tickSpacing), tickSpacing)
const minIndex = useMemo(
() =>
tickSpacing && activeTick ? bitmapIndex(activeTick - numSurroundingTicks * tickSpacing, tickSpacing) : undefined,
[tickSpacing, activeTick, numSurroundingTicks]
)
const maxIndex = useMemo(
() =>
tickSpacing && activeTick ? bitmapIndex(activeTick + numSurroundingTicks * tickSpacing, tickSpacing) : undefined,
[tickSpacing, activeTick, numSurroundingTicks]
)
const tickLensArgs: [string, number][] = useMemo(
() =>
maxIndex && minIndex && poolAddress && poolAddress !== ZERO_ADDRESS
? new Array(maxIndex - minIndex + 1)
.fill(0)
.map((_, i) => i + minIndex)
.map((wordIndex) => [poolAddress, wordIndex])
: [],
[minIndex, maxIndex, poolAddress]
)
const tickLens = useTickLens()
const callStates = useSingleContractMultipleData(
tickLensArgs.length > 0 ? tickLens : undefined,
'getPopulatedTicksInWord',
tickLensArgs,
REFRESH_FREQUENCY
)
const isError = useMemo(() => callStates.some(({ error }) => error), [callStates])
const isLoading = useMemo(() => callStates.some(({ loading }) => loading), [callStates])
const IsSyncing = useMemo(() => callStates.some(({ syncing }) => syncing), [callStates])
const isValid = useMemo(() => callStates.some(({ valid }) => valid), [callStates])
const tickData: TickData[] = useMemo(
() =>
callStates
.map(({ result }) => result?.populatedTicks)
.reduce(
(accumulator, current) => [
...accumulator,
...(current?.map((tickData: TickData) => {
return {
tick: tickData.tick,
liquidityNet: JSBI.BigInt(tickData.liquidityNet),
liquidityGross: JSBI.BigInt(tickData.liquidityGross),
}
}) ?? []),
],
[]
),
[callStates]
)
// return the latest synced tickData even if we are still loading the newest data
useEffect(() => {
if (!IsSyncing && !isLoading && !isError && isValid) {
setTickDataLatestSynced(tickData.sort((a, b) => a.tick - b.tick))
}
}, [isError, isLoading, IsSyncing, tickData, isValid])
return useMemo(
() => ({ isLoading, IsSyncing, isError, isValid, tickData: tickDataLatestSynced }),
[isLoading, IsSyncing, isError, isValid, tickDataLatestSynced]
)
}
function useTicksFromSubgraph(
currencyA: Currency | undefined,
currencyB: Currency | undefined,
feeAmount: FeeAmount | undefined
@ -32,19 +138,34 @@ export function useAllV3Ticks(
const poolAddress =
currencyA && currencyB && feeAmount ? Pool.getAddress(currencyA?.wrapped, currencyB?.wrapped, feeAmount) : undefined
const { isLoading, isError, error, isUninitialized, data } = useAllV3TicksQuery(
poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken,
{
pollingInterval: ms`30s`,
}
)
return useAllV3TicksQuery(poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken, {
pollingInterval: ms`30s`,
})
}
// Fetches all ticks for a given pool
function useAllV3Ticks(
currencyA: Currency | undefined,
currencyB: Currency | undefined,
feeAmount: FeeAmount | undefined
): {
isLoading: boolean
isUninitialized: boolean
isError: boolean
error: unknown
ticks: TickData[] | undefined
} {
const useSubgraph = currencyA ? !CHAIN_IDS_MISSING_SUBGRAPH_DATA.includes(currencyA.chainId) : true
const tickLensTickData = useTicksFromTickLens(!useSubgraph ? currencyA : undefined, currencyB, feeAmount)
const subgraphTickData = useTicksFromSubgraph(useSubgraph ? currencyA : undefined, currencyB, feeAmount)
return {
isLoading,
isUninitialized,
isError,
error,
ticks: data?.ticks as AllV3TicksQuery['ticks'],
isLoading: useSubgraph ? subgraphTickData.isLoading : tickLensTickData.isLoading,
isUninitialized: useSubgraph ? subgraphTickData.isUninitialized : false,
isError: useSubgraph ? subgraphTickData.isError : tickLensTickData.isError,
error: useSubgraph ? subgraphTickData.error : tickLensTickData.isError,
ticks: useSubgraph ? subgraphTickData.data?.ticks : tickLensTickData.tickData,
}
}
@ -94,7 +215,7 @@ export function usePoolActiveLiquidity(
// 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 = ticks.findIndex(({ tickIdx }) => tickIdx > activeTick) - 1
const pivot = ticks.findIndex(({ tick }) => tick > activeTick) - 1
if (pivot < 0) {
// consider setting a local error
@ -111,9 +232,8 @@ export function usePoolActiveLiquidity(
const activeTickProcessed: TickProcessed = {
liquidityActive: JSBI.BigInt(pool[1]?.liquidity ?? 0),
tickIdx: activeTick,
liquidityNet:
Number(ticks[pivot].tickIdx) === activeTick ? JSBI.BigInt(ticks[pivot].liquidityNet) : JSBI.BigInt(0),
tick: activeTick,
liquidityNet: Number(ticks[pivot].tick) === activeTick ? JSBI.BigInt(ticks[pivot].liquidityNet) : JSBI.BigInt(0),
price0: tickToPrice(token0, token1, activeTick).toFixed(PRICE_FIXED_DIGITS),
}

@ -26,7 +26,7 @@ export const api = createApi({
document: gql`
query allV3Ticks($poolAddress: String!, $skip: Int!) {
ticks(first: 1000, skip: $skip, where: { poolAddress: $poolAddress }, orderBy: tickIdx) {
tickIdx
tick: tickIdx
liquidityNet
price0
price1

@ -1,15 +1,14 @@
import { Token } from '@uniswap/sdk-core'
import { FeeAmount, TICK_SPACINGS } from '@uniswap/v3-sdk'
import { TickData, TickProcessed } from 'hooks/usePoolTickData'
import JSBI from 'jsbi'
import { AllV3TicksQuery } from 'state/data/generated'
import computeSurroundingTicks from './computeSurroundingTicks'
const getV3Tick = (tickIdx: number, liquidityNet: number) => ({
tickIdx,
const getV3Tick = (tick: number, liquidityNet: number): TickData => ({
tick,
liquidityNet: JSBI.BigInt(liquidityNet),
price0: '1',
price1: '2',
liquidityGross: JSBI.BigInt(liquidityNet),
})
describe('#computeSurroundingTicks', () => {
@ -18,22 +17,22 @@ describe('#computeSurroundingTicks', () => {
const token1 = new Token(1, '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', 18)
const feeAmount = FeeAmount.LOW
const spacing = TICK_SPACINGS[feeAmount]
const activeTickProcessed = {
tickIdx: 1000,
const activeTickProcessed: TickProcessed = {
tick: 1000,
liquidityActive: JSBI.BigInt(300),
liquidityNet: JSBI.BigInt(100),
price0: '100',
}
const pivot = 3
const ascending = true
const sortedTickData: AllV3TicksQuery['ticks'] = [
getV3Tick(activeTickProcessed.tickIdx - 4 * spacing, 10),
getV3Tick(activeTickProcessed.tickIdx - 2 * spacing, 20),
getV3Tick(activeTickProcessed.tickIdx - 1 * spacing, 30),
getV3Tick(activeTickProcessed.tickIdx * spacing, 100),
getV3Tick(activeTickProcessed.tickIdx + 1 * spacing, 40),
getV3Tick(activeTickProcessed.tickIdx + 2 * spacing, 20),
getV3Tick(activeTickProcessed.tickIdx + 5 * spacing, 20),
const sortedTickData: TickData[] = [
getV3Tick(activeTickProcessed.tick - 4 * spacing, 10),
getV3Tick(activeTickProcessed.tick - 2 * spacing, 20),
getV3Tick(activeTickProcessed.tick - 1 * spacing, 30),
getV3Tick(activeTickProcessed.tick * spacing, 100),
getV3Tick(activeTickProcessed.tick + 1 * spacing, 40),
getV3Tick(activeTickProcessed.tick + 2 * spacing, 20),
getV3Tick(activeTickProcessed.tick + 5 * spacing, 20),
]
const previous = computeSurroundingTicks(token0, token1, activeTickProcessed, sortedTickData, pivot, !ascending)
@ -41,17 +40,17 @@ describe('#computeSurroundingTicks', () => {
const subsequent = computeSurroundingTicks(token0, token1, activeTickProcessed, sortedTickData, pivot, ascending)
expect(previous.length).toEqual(3)
expect(previous.map((t) => [t.tickIdx, parseFloat(t.liquidityActive.toString())])).toEqual([
[activeTickProcessed.tickIdx - 4 * spacing, 150],
[activeTickProcessed.tickIdx - 2 * spacing, 170],
[activeTickProcessed.tickIdx - 1 * spacing, 200],
expect(previous.map((t) => [t.tick, parseFloat(t.liquidityActive.toString())])).toEqual([
[activeTickProcessed.tick - 4 * spacing, 150],
[activeTickProcessed.tick - 2 * spacing, 170],
[activeTickProcessed.tick - 1 * spacing, 200],
])
expect(subsequent.length).toEqual(3)
expect(subsequent.map((t) => [t.tickIdx, parseFloat(t.liquidityActive.toString())])).toEqual([
[activeTickProcessed.tickIdx + 1 * spacing, 340],
[activeTickProcessed.tickIdx + 2 * spacing, 360],
[activeTickProcessed.tickIdx + 5 * spacing, 380],
expect(subsequent.map((t) => [t.tick, parseFloat(t.liquidityActive.toString())])).toEqual([
[activeTickProcessed.tick + 1 * spacing, 340],
[activeTickProcessed.tick + 2 * spacing, 360],
[activeTickProcessed.tick + 5 * spacing, 380],
])
})
})

@ -1,8 +1,7 @@
import { Token } from '@uniswap/sdk-core'
import { tickToPrice } from '@uniswap/v3-sdk'
import { TickProcessed } from 'hooks/usePoolTickData'
import { TickData, TickProcessed } from 'hooks/usePoolTickData'
import JSBI from 'jsbi'
import { AllV3TicksQuery } from 'state/data/generated'
const PRICE_FIXED_DIGITS = 8
@ -11,7 +10,7 @@ export default function computeSurroundingTicks(
token0: Token,
token1: Token,
activeTickProcessed: TickProcessed,
sortedTickData: AllV3TicksQuery['ticks'],
sortedTickData: TickData[],
pivot: number,
ascending: boolean
): TickProcessed[] {
@ -22,12 +21,12 @@ export default function computeSurroundingTicks(
// 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 tick = Number(sortedTickData[i].tick)
const currentTickProcessed: TickProcessed = {
liquidityActive: previousTickProcessed.liquidityActive,
tickIdx,
tick,
liquidityNet: JSBI.BigInt(sortedTickData[i].liquidityNet),
price0: tickToPrice(token0, token1, tickIdx).toFixed(PRICE_FIXED_DIGITS),
price0: tickToPrice(token0, token1, tick).toFixed(PRICE_FIXED_DIGITS),
}
// Update the active liquidity.