import gql from 'graphql-tag' ; import JSBI from 'jsbi' ; import keyBy from 'lodash.keyby' ; import { TickMath , tickToPrice } from '@uniswap/v3-sdk' ; import { Token } from '@uniswap/sdk-core' ; import { ApolloClient , NormalizedCacheObject } from '@apollo/client' ; const PRICE_FIXED_DIGITS = 4 ; const DEFAULT_SURROUNDING_TICKS = 300 ; const FEE_TIER_TO_TICK_SPACING = ( feeTier ) : number => { switch ( feeTier ) { case '10000' : return 200 ; case '3000' : return 60 ; case '500' : return 10 ; case '100' : return 1 ; default : throw Error ( `Tick spacing for fee tier ${ feeTier } undefined.` ) ; } } ; interface TickPool { tick : string ; feeTier : string ; token0 : { symbol : string ; id : string ; decimals : string ; } ; token1 : { symbol : string ; id : string ; decimals : string ; } ; sqrtPrice : string ; liquidity : string ; } interface PoolResult { pool : TickPool ; } interface Tick { tickIdx : string ; liquidityGross : string ; liquidityNet : string ; price0 : string ; price1 : string ; } interface SurroundingTicksResult { ticks : Tick [ ] ; } export interface TickProcessed { liquidityGross : JSBI ; liquidityNet : JSBI ; tickIdx : number ; liquidityActive : JSBI ; price0 : string ; price1 : string ; } const fetchInitializedTicks = async ( poolAddress , tickIdxLowerBound , tickIdxUpperBound , client ) : Promise < { loading ? : boolean ; error ? : boolean ; ticks ? : Tick [ ] ; } > => { const tickQuery = gql `
    query surroundingTicks(
      $poolAddress: String!
      $tickIdxLowerBound: BigInt!
      $tickIdxUpperBound: BigInt!
      $skip: Int!
    ) {
      ticks(
        subgraphError: allow
        first: 1000
        skip: $skip
        where: { poolAddress: $poolAddress, tickIdx_lte: $tickIdxUpperBound, tickIdx_gte: $tickIdxLowerBound }
      ) {
        tickIdx
        liquidityGross
        liquidityNet
        price0
        price1
      }
    }
  ` ; let surroundingTicks = [ ] ; let surroundingTicksResult = [ ] ; let skip = 0 ; do { const { data , error , loading } = await client . query < SurroundingTicksResult > ( { query : tickQuery , fetchPolicy : 'cache-first' , variables : { poolAddress , tickIdxLowerBound , tickIdxUpperBound , skip , } , } ) ; if ( loading ) { continue ; } if ( error ) { return { error : Boolean ( error ) , loading , ticks : surroundingTicksResult } ; } surroundingTicks = data . ticks ; surroundingTicksResult = surroundingTicksResult . concat ( surroundingTicks ) ; skip += 1000 ; } while ( surroundingTicks . length > 0 ) ; return { ticks : surroundingTicksResult , loading : false , error : false } ; } ; export interface PoolTickData { ticksProcessed : TickProcessed [ ] ; feeTier : string ; tickSpacing : number ; activeTickIdx : number ; } const poolQuery = gql `
  query pool($poolAddress: String!) {
    pool(id: $poolAddress) {
      tick
      token0 {
        symbol
        id
        decimals
      }
      token1 {
        symbol
        id
        decimals
      }
      feeTier
      sqrtPrice
      liquidity
    }
  }
` ; export const fetchTicksSurroundingPrice = async ( poolAddress , client , numSurroundingTicks = DEFAULT_SURROUNDING_TICKS ) : Promise < { loading ? : boolean ; error ? : boolean ; data ? : PoolTickData ; } > => { const { data : poolResult , error , loading } = await client . query < PoolResult > ( { query : poolQuery , variables : { poolAddress , } , } ) ; if ( loading || error || ! poolResult ) { return { loading , error : Boolean ( error ) , data : undefined , } ; } const { pool : { tick : poolCurrentTick , feeTier , liquidity , token0 : { id : token0Address , decimals : token0Decimals } , token1 : { id : token1Address , decimals : token1Decimals } , } , } = poolResult ; const poolCurrentTickIdx = parseInt ( poolCurrentTick ) ; const tickSpacing = FEE_TIER_TO_TICK_SPACING ( feeTier ) ; const activeTickIdx = Math . floor ( poolCurrentTickIdx / tickSpacing ) * tickSpacing ; const tickIdxLowerBound = activeTickIdx - numSurroundingTicks * tickSpacing ; const tickIdxUpperBound = activeTickIdx + numSurroundingTicks * tickSpacing ; const initializedTicksResult = await fetchInitializedTicks ( poolAddress , tickIdxLowerBound , tickIdxUpperBound , client ) ; if ( initializedTicksResult . error || initializedTicksResult . loading ) { return { error : initializedTicksResult . error , loading : initializedTicksResult . loading , } ; } const { ticks : initializedTicks } = initializedTicksResult ; const tickIdxToInitializedTick = keyBy ( initializedTicks , 'tickIdx' ) ; const token0 = new Token ( 1 , token0Address , parseInt ( token0Decimals ) ) ; const token1 = new Token ( 1 , token1Address , parseInt ( token1Decimals ) ) ; let activeTickIdxForPrice = activeTickIdx ; if ( activeTickIdxForPrice < TickMath . MIN_TICK ) { activeTickIdxForPrice = TickMath . MIN_TICK ; } if ( activeTickIdxForPrice > TickMath . MAX_TICK ) { activeTickIdxForPrice = TickMath . MAX_TICK ; } const activeTickProcessed = { liquidityActive : JSBI . BigInt ( liquidity ) , tickIdx : activeTickIdx , liquidityNet : JSBI . BigInt ( 0 ) , price0 : tickToPrice ( token0 , token1 , activeTickIdxForPrice ) . toFixed ( PRICE_FIXED_DIGITS ) , price1 : tickToPrice ( token1 , token0 , activeTickIdxForPrice ) . toFixed ( PRICE_FIXED_DIGITS ) , liquidityGross : JSBI . BigInt ( 0 ) , } ; const activeTick = tickIdxToInitializedTick [ activeTickIdx ] ; if ( activeTick ) { activeTickProcessed . liquidityGross = JSBI . BigInt ( activeTick . liquidityGross ) ; activeTickProcessed . liquidityNet = JSBI . BigInt ( activeTick . liquidityNet ) ; } enum Direction { ASC , DESC } const computeSurroundingTicks = ( activeTickProcessed , tickSpacing , numSurroundingTicks , direction ) => { let previousTickProcessed = { ... activeTickProcessed , } ; let processedTicks = [ ] ; for ( let i = 0 ; i < numSurroundingTicks ; i ++ ) { const currentTickIdx = direction == Direction . ASC ? previousTickProcessed . tickIdx + tickSpacing : previousTickProcessed . tickIdx - tickSpacing ; if ( currentTickIdx < TickMath . MIN_TICK || currentTickIdx > TickMath . MAX_TICK ) { break ; } const currentTickProcessed = { liquidityActive : previousTickProcessed . liquidityActive , tickIdx : currentTickIdx , liquidityNet : JSBI . BigInt ( 0 ) , price0 : tickToPrice ( token0 , token1 , currentTickIdx ) . toFixed ( PRICE_FIXED_DIGITS ) , price1 : tickToPrice ( token1 , token0 , currentTickIdx ) . toFixed ( PRICE_FIXED_DIGITS ) , liquidityGross : JSBI . BigInt ( 0 ) , } ; const currentInitializedTick = tickIdxToInitializedTick [ currentTickIdx . toString ( ) ] ; if ( currentInitializedTick ) { currentTickProcessed . liquidityGross = JSBI . BigInt ( currentInitializedTick . liquidityGross ) ; currentTickProcessed . liquidityNet = JSBI . BigInt ( currentInitializedTick . liquidityNet ) ; } if ( direction == Direction . ASC && currentInitializedTick ) { currentTickProcessed . liquidityActive = JSBI . add ( previousTickProcessed . liquidityActive , JSBI . BigInt ( currentInitializedTick . liquidityNet ) ) ; } else if ( direction == Direction . DESC && JSBI . notEqual ( previousTickProcessed . liquidityNet , JSBI . BigInt ( 0 ) ) ) { currentTickProcessed . liquidityActive = JSBI . subtract ( previousTickProcessed . liquidityActive , previousTickProcessed . liquidityNet ) ; } processedTicks . push ( currentTickProcessed ) ; previousTickProcessed = currentTickProcessed ; } if ( direction == Direction . DESC ) { processedTicks = processedTicks . reverse ( ) ; } return processedTicks ; } ; const subsequentTicks = computeSurroundingTicks ( activeTickProcessed , tickSpacing , numSurroundingTicks , Direction . ASC ) ; const previousTicks = computeSurroundingTicks ( activeTickProcessed , tickSpacing , numSurroundingTicks , Direction . DESC ) ; const ticksProcessed = previousTicks . concat ( activeTickProcessed ) . concat ( subsequentTicks ) ; return { data : { ticksProcessed , feeTier , tickSpacing , activeTickIdx , } , } ; } ;