feat: use TickLens on chains where subgraph is not functional (#3149)
This commit is contained in:
parent
b878d764e5
commit
c9642c6cd0
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user