diff --git a/src/components/LiquidityChartRangeInput/hooks.ts b/src/components/LiquidityChartRangeInput/hooks.ts index 4996b2ea7d..c8d6a204c2 100644 --- a/src/components/LiquidityChartRangeInput/hooks.ts +++ b/src/components/LiquidityChartRangeInput/hooks.ts @@ -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, diff --git a/src/constants/addresses.ts b/src/constants/addresses.ts index 7d2bcfaf50..fd35155757 100644 --- a/src/constants/addresses.ts +++ b/src/constants/addresses.ts @@ -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', +} diff --git a/src/hooks/useContract.ts b/src/hooks/useContract.ts index 0d0ddbe088..fc25337f17 100644 --- a/src/hooks/useContract.ts +++ b/src/hooks/useContract.ts @@ -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_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 +} diff --git a/src/hooks/usePoolTickData.ts b/src/hooks/usePoolTickData.ts index 334c95f00d..24d02e5edb 100644 --- a/src/hooks/usePoolTickData.ts +++ b/src/hooks/usePoolTickData.ts @@ -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([]) + + 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), } diff --git a/src/state/data/slice.ts b/src/state/data/slice.ts index 165e2dcb87..d39be94b74 100644 --- a/src/state/data/slice.ts +++ b/src/state/data/slice.ts @@ -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 diff --git a/src/utils/computeSurroundingTicks.test.ts b/src/utils/computeSurroundingTicks.test.ts index 1aaa071dfd..dc21eddd0a 100644 --- a/src/utils/computeSurroundingTicks.test.ts +++ b/src/utils/computeSurroundingTicks.test.ts @@ -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], ]) }) }) diff --git a/src/utils/computeSurroundingTicks.ts b/src/utils/computeSurroundingTicks.ts index 768ab63fdb..50c653c77c 100644 --- a/src/utils/computeSurroundingTicks.ts +++ b/src/utils/computeSurroundingTicks.ts @@ -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.