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 { Currency } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk' import { FeeAmount } from '@uniswap/v3-sdk'
import { usePoolActiveLiquidity } from 'hooks/usePoolTickData' import { TickProcessed, usePoolActiveLiquidity } from 'hooks/usePoolTickData'
import JSBI from 'jsbi'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { ChartEntry } from './types' import { ChartEntry } from './types'
export interface TickProcessed {
liquidityActive: JSBI
price0: string
}
export function useDensityChartData({ export function useDensityChartData({
currencyA, currencyA,
currencyB, currencyB,

@ -107,3 +107,8 @@ export const V3_MIGRATOR_ADDRESSES: AddressMap = constructSameAddressMap('0xA564
SupportedChainId.POLYGON_MUMBAI, SupportedChainId.POLYGON_MUMBAI,
SupportedChainId.POLYGON, 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 IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.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 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 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 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' import { abi as V2MigratorABI } from '@uniswap/v3-periphery/artifacts/contracts/V3Migrator.sol/V3Migrator.json'
@ -30,13 +31,14 @@ import {
MULTICALL_ADDRESS, MULTICALL_ADDRESS,
NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES,
QUOTER_ADDRESSES, QUOTER_ADDRESSES,
TICK_LENS_ADDRESSES,
V2_ROUTER_ADDRESS, V2_ROUTER_ADDRESS,
V3_MIGRATOR_ADDRESSES, V3_MIGRATOR_ADDRESSES,
} from 'constants/addresses' } from 'constants/addresses'
import { UNI, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import { UNI, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import useActiveWeb3React from 'hooks/useActiveWeb3React' import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useMemo } from 'react' 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 { V3Migrator } from 'types/v3/V3Migrator'
import { getContract } from '../utils' import { getContract } from '../utils'
@ -159,3 +161,9 @@ export function useV3NFTPositionManagerContract(withSignerIfPossible?: boolean):
export function useV3Quoter() { export function useV3Quoter() {
return useContract<Quoter>(QUOTER_ADDRESSES, QuoterABI) 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 { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency } from '@uniswap/sdk-core' 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 JSBI from 'jsbi'
import { useSingleContractMultipleData } from 'lib/hooks/multicall'
import ms from 'ms.macro' import ms from 'ms.macro'
import { useMemo } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useAllV3TicksQuery } from 'state/data/enhanced' import { useAllV3TicksQuery } from 'state/data/enhanced'
import { AllV3TicksQuery } from 'state/data/generated'
import computeSurroundingTicks from 'utils/computeSurroundingTicks' import computeSurroundingTicks from 'utils/computeSurroundingTicks'
import { useTickLens } from './useContract'
import { PoolState, usePool } from './usePools' import { PoolState, usePool } from './usePools'
const PRICE_FIXED_DIGITS = 8 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. // Tick with fields parsed to JSBIs, and active liquidity computed.
export interface TickProcessed { export interface TickProcessed {
tickIdx: number tick: number
liquidityActive: JSBI liquidityActive: JSBI
liquidityNet: JSBI liquidityNet: JSBI
price0: string price0: string
} }
const REFRESH_FREQUENCY = { blocksPerFetch: 2 }
const getActiveTick = (tickCurrent: number | undefined, feeAmount: FeeAmount | undefined) => const getActiveTick = (tickCurrent: number | undefined, feeAmount: FeeAmount | undefined) =>
tickCurrent && feeAmount ? Math.floor(tickCurrent / TICK_SPACINGS[feeAmount]) * TICK_SPACINGS[feeAmount] : undefined tickCurrent && feeAmount ? Math.floor(tickCurrent / TICK_SPACINGS[feeAmount]) * TICK_SPACINGS[feeAmount] : undefined
// Fetches all ticks for a given pool const bitmapIndex = (tick: number, tickSpacing: number) => {
export function useAllV3Ticks( 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, currencyA: Currency | undefined,
currencyB: Currency | undefined, currencyB: Currency | undefined,
feeAmount: FeeAmount | undefined feeAmount: FeeAmount | undefined
@ -32,19 +138,34 @@ export function useAllV3Ticks(
const poolAddress = const poolAddress =
currencyA && currencyB && feeAmount ? Pool.getAddress(currencyA?.wrapped, currencyB?.wrapped, feeAmount) : undefined currencyA && currencyB && feeAmount ? Pool.getAddress(currencyA?.wrapped, currencyB?.wrapped, feeAmount) : undefined
const { isLoading, isError, error, isUninitialized, data } = useAllV3TicksQuery( return useAllV3TicksQuery(poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken, {
poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken, pollingInterval: ms`30s`,
{ })
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 { return {
isLoading, isLoading: useSubgraph ? subgraphTickData.isLoading : tickLensTickData.isLoading,
isUninitialized, isUninitialized: useSubgraph ? subgraphTickData.isUninitialized : false,
isError, isError: useSubgraph ? subgraphTickData.isError : tickLensTickData.isError,
error, error: useSubgraph ? subgraphTickData.error : tickLensTickData.isError,
ticks: data?.ticks as AllV3TicksQuery['ticks'], 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 // find where the active tick would be to partition the array
// if the active tick is initialized, the pivot will be an element // if the active tick is initialized, the pivot will be an element
// if not, take the previous tick as pivot // 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) { if (pivot < 0) {
// consider setting a local error // consider setting a local error
@ -111,9 +232,8 @@ export function usePoolActiveLiquidity(
const activeTickProcessed: TickProcessed = { const activeTickProcessed: TickProcessed = {
liquidityActive: JSBI.BigInt(pool[1]?.liquidity ?? 0), liquidityActive: JSBI.BigInt(pool[1]?.liquidity ?? 0),
tickIdx: activeTick, tick: activeTick,
liquidityNet: liquidityNet: Number(ticks[pivot].tick) === activeTick ? JSBI.BigInt(ticks[pivot].liquidityNet) : JSBI.BigInt(0),
Number(ticks[pivot].tickIdx) === activeTick ? JSBI.BigInt(ticks[pivot].liquidityNet) : JSBI.BigInt(0),
price0: tickToPrice(token0, token1, activeTick).toFixed(PRICE_FIXED_DIGITS), price0: tickToPrice(token0, token1, activeTick).toFixed(PRICE_FIXED_DIGITS),
} }

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

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

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