I tried to analysis pool information and I want to make a result like below.
I wrote the code below by referring to the uniswap sdk documentation.
import JSBI from "jsbi";
import axios from "axios";
import IUniswapV3PoolABI from "@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json";
import { ethers } from "ethers";
import { ChainId, Token, CurrencyAmount, WETH9 } from "@uniswap/sdk-core";
import { Tick, Pool, TickMath, FeeAmount, tickToPrice } from "@uniswap/v3-sdk";
// -----------------------config------------------------------------
// Currencies and Tokens
export const WETH_TOKEN = new Token(
ChainId.MAINNET,
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
18,
"WETH",
"Wrapped Ether"
);
export const WBTC_TOKEN = new Token(
ChainId.MAINNET,
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
8,
"WBTC",
"Wrapped BTC"
);
export const USDC_TOKEN = new Token(
ChainId.MAINNET,
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
6,
"USDC",
"USD//C"
);
// Sets if the example should run locally or on chain
export enum Environment {
MAINNET,
}
// Inputs that configure this example to run
export interface ExampleConfig {
env: Environment;
rpc: {
mainnet: string;
};
pool: {
token0: Token;
token1: Token;
fee: FeeAmount;
};
chart: {
numSurroundingTicks: number;
};
}
// Example Configuration
export const CurrentConfig: ExampleConfig = {
env: Environment.MAINNET,
rpc: {
mainnet: "https://mainnet.infura.io/v3/d34caea371204e09a0accdc2834033ee",
},
pool: {
token0: WBTC_TOKEN,
// token1: WETH_TOKEN,
token1: WETH_TOKEN,
// fee: FeeAmount.LOW,
fee: FeeAmount.MEDIUM,
},
chart: {
numSurroundingTicks: 3,
},
};
// -----------------------config------------------------------------
enum Direction {
ASC,
DESC,
}
interface GraphTick {
tickIdx: string;
liquidityGross: string;
liquidityNet: string;
}
interface TickProcessed {
tickIdx: number;
liquidityActive: JSBI;
liquidityNet: JSBI;
price0: number;
price1: number;
isCurrent: boolean;
}
export interface BarChartTick {
tickIdx: number;
liquidityActive: number;
liquidityLockedToken0: number;
liquidityLockedToken1: number;
price0: number;
price1: number;
isCurrent: boolean;
}
const MAX_INT128 = JSBI.subtract(
JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(128)),
JSBI.BigInt(1)
);
export async function getFullPool(): Promise<{
pool: Pool;
ticks: BarChartTick[];
}> {
// const poolAddress = "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640";
const poolAddress = "0xCBCdF9626bC03E24f779434178A73a0B4bad62eD";
const ethersProvider = new ethers.providers.JsonRpcProvider(
"https://mainnet.infura.io/v3/{api-key}"
);
const poolContract = new ethers.Contract(
poolAddress,
IUniswapV3PoolABI.abi,
ethersProvider
);
const [slot0, liquidity, graphTicks] = await Promise.all([
poolContract.slot0(),
poolContract.liquidity(),
getFullTickData(poolAddress),
]);
const sdkTicks = graphTicks.map((graphTick: GraphTick) => {
return new Tick({
index: +graphTick.tickIdx,
liquidityGross: graphTick.liquidityGross,
liquidityNet: graphTick.liquidityNet,
});
});
const fullPool = new Pool(
CurrentConfig.pool.token0,
CurrentConfig.pool.token1,
CurrentConfig.pool.fee,
slot0.sqrtPriceX96,
liquidity,
slot0.tick,
sdkTicks
);
const tickSpacing = fullPool.tickSpacing;
const activeTickIdx = (
await fullPool.tickDataProvider.nextInitializedTickWithinOneWord(
fullPool.tickCurrent,
fullPool.tickCurrent === TickMath.MIN_TICK ? false : true,
tickSpacing
)
)[0];
const barChartTicks = await createBarChartTicks(
activeTickIdx,
fullPool.liquidity,
tickSpacing,
fullPool.token0,
fullPool.token1,
3,
fullPool.fee,
graphTicks
);
return {
pool: fullPool,
ticks: barChartTicks,
};
}
async function getFullTickData(poolAddress: string): Promise<GraphTick[]> {
let allTicks: GraphTick[] = [];
let skip = 0;
let loadingTicks = true;
while (loadingTicks) {
const ticks = await getTickDataFromSubgraph(poolAddress, skip);
allTicks = allTicks.concat(ticks);
if (ticks.length < 1000) {
loadingTicks = false;
} else {
skip += 1000;
}
}
return allTicks;
}
async function getTickDataFromSubgraph(
poolAddress: string,
skip: number
): Promise<GraphTick[]> {
const ticksQuery = JSON.stringify({
query: `{ ticks(
where: {poolAddress: "${poolAddress.toLowerCase()}", liquidityNet_not: "0"}
first: 1000,
orderBy: tickIdx,
orderDirection: asc,
skip: ${skip}
) {
tickIdx
liquidityGross
liquidityNet
}
}
`,
});
const response = await axios.post(
"https://gateway.thegraph.com/api/{api-key}/subgraphs/id/5zvR82QoaXYFyDEKLZ9t6v9adgnptxYpKpSbxtgVENFV",
ticksQuery,
{
headers: {
"Content-Type": "application/json",
},
}
);
return response.data.data.ticks;
}
export async function createBarChartTicks(
tickCurrent: number,
poolLiquidity: JSBI,
tickSpacing: number,
token0: Token,
token1: Token,
numSurroundingTicks: number,
feeTier: FeeAmount,
graphTicks: GraphTick[]
): Promise<BarChartTick[]> {
const processedTicks = processTicks(
tickCurrent,
poolLiquidity,
tickSpacing,
token0,
token1,
numSurroundingTicks,
graphTicks
);
const barTicks = await Promise.all(
processedTicks.map(async (tick: TickProcessed) => {
return calculateLockedLiqudity(
tick,
token0,
token1,
tickSpacing,
feeTier
);
})
);
barTicks.map((entry, i) => {
if (i > 0) {
barTicks[i - 1].liquidityLockedToken0 = entry.liquidityLockedToken0;
barTicks[i - 1].liquidityLockedToken1 = entry.liquidityLockedToken1;
}
});
return barTicks;
}
function processTicks(
tickCurrent: number,
poolLiquidity: JSBI,
tickSpacing: number,
token0: Token,
token1: Token,
numSurroundingTicks: number,
graphTicks: GraphTick[]
): TickProcessed[] {
const tickIdxToTickDictionary: Record<string, GraphTick> = Object.fromEntries(
graphTicks.map((graphTick) => [graphTick.tickIdx, graphTick])
);
const liquidity = poolLiquidity;
let activeTickIdx = Math.floor(tickCurrent / tickSpacing) * tickSpacing;
if (activeTickIdx <= TickMath.MIN_TICK) {
activeTickIdx = TickMath.MAX_TICK;
}
const activeTickProcessed: TickProcessed = {
tickIdx: activeTickIdx,
liquidityActive: liquidity,
liquidityNet: JSBI.BigInt(0),
price0: parseFloat(
tickToPrice(token0, token1, activeTickIdx).toSignificant(6)
),
price1: parseFloat(
tickToPrice(token1, token0, activeTickIdx).toSignificant(6)
),
isCurrent: true,
};
const activeTick = tickIdxToTickDictionary[activeTickIdx];
if (activeTick) {
activeTickProcessed.liquidityNet = JSBI.BigInt(activeTick.liquidityNet);
}
const subsequentTicks: TickProcessed[] = computeInitializedTicks(
activeTickProcessed,
numSurroundingTicks,
tickSpacing,
Direction.ASC,
token0,
token1,
tickIdxToTickDictionary
);
const previousTicks: TickProcessed[] = computeInitializedTicks(
activeTickProcessed,
numSurroundingTicks,
tickSpacing,
Direction.DESC,
token0,
token1,
tickIdxToTickDictionary
);
const TickProcesseds = previousTicks
.concat(activeTickProcessed)
.concat(subsequentTicks);
return TickProcesseds;
}
function computeInitializedTicks(
activeTickProcessed: TickProcessed,
numSurroundingTicks: number,
tickSpacing: number,
direction: Direction,
token0: Token,
token1: Token,
tickIdxToTickDictionary: Record<string, GraphTick>
): TickProcessed[] {
let previousTickProcessed: TickProcessed = {
...activeTickProcessed,
};
let ticksProcessed: TickProcessed[] = [];
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: TickProcessed = {
tickIdx: currentTickIdx,
liquidityActive: previousTickProcessed.liquidityActive,
liquidityNet: JSBI.BigInt(0),
price0: parseFloat(
tickToPrice(token0, token1, currentTickIdx).toSignificant(6)
),
price1: parseFloat(
tickToPrice(token1, token0, currentTickIdx).toSignificant(6)
),
isCurrent: false,
};
const currentInitializedTick =
tickIdxToTickDictionary[currentTickIdx.toString()];
if (currentInitializedTick) {
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))
) {
// We are iterating descending, so look at the previous tick and apply any net liquidity.
currentTickProcessed.liquidityActive = JSBI.subtract(
previousTickProcessed.liquidityActive,
previousTickProcessed.liquidityNet
);
}
ticksProcessed.push(currentTickProcessed);
previousTickProcessed = currentTickProcessed;
}
if (direction == Direction.DESC) {
ticksProcessed = ticksProcessed.reverse();
}
return ticksProcessed;
}
async function calculateLockedLiqudity(
tick: TickProcessed,
token0: Token,
token1: Token,
tickSpacing: number,
feeTier: FeeAmount
): Promise<BarChartTick> {
const sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick.tickIdx);
const liqGross = JSBI.greaterThan(tick.liquidityNet, JSBI.BigInt(0))
? tick.liquidityNet
: JSBI.multiply(tick.liquidityNet, JSBI.BigInt("-1"));
const mockTicks = [
{
index: tick.tickIdx - tickSpacing,
liquidityGross: liqGross,
liquidityNet: JSBI.multiply(tick.liquidityNet, JSBI.BigInt("-1")),
},
{
index: tick.tickIdx,
liquidityGross: liqGross,
liquidityNet: tick.liquidityNet,
},
// {
// index: tick.tickIdx + tickSpacing,
// liquidityGross: liqGross,
// liquidityNet: JSBI.multiply(tick.liquidityNet, JSBI.BigInt("-1")),
// },
];
const pool =
token0 && token1 && feeTier
? new Pool(
token0,
token1,
feeTier,
// 0,
// 0,
sqrtPriceX96,
tick.liquidityActive,
tick.tickIdx,
mockTicks
)
: undefined;
const prevSqrtX96 = TickMath.getSqrtRatioAtTick(tick.tickIdx - tickSpacing);
const maxAmountToken0 = token0
? CurrencyAmount.fromRawAmount(token0, MAX_INT128.toString())
: undefined;
const outputRes0 =
pool && maxAmountToken0
? await pool.getOutputAmount(maxAmountToken0, prevSqrtX96)
: undefined;
const token1Amount = outputRes0?.[0] as CurrencyAmount<Token> | undefined;
const amount0 = token1Amount
? parseFloat(token1Amount.toExact()) * tick.price1
: 0;
const amount1 = token1Amount ? parseFloat(token1Amount.toExact()) : 0;
return {
tickIdx: tick.tickIdx,
liquidityActive: parseFloat(tick.liquidityActive.toString()),
liquidityLockedToken0: amount0,
liquidityLockedToken1: amount1,
price0: tick.price0,
price1: tick.price1,
isCurrent: tick.isCurrent,
};
}
(async () => {
await getFullPool();
})();
What I wanted was to calculate the data of the image directly.
But my final result in variables barChartTicks is different with capture
for example :
what I calculated in tick 1WBTC = 23.0771WETH is
WBTC liquidity : 14.55630885..
WETH liquidity : 337.93881....
those are different with pool information in uniswap webpage(the first capture )
WBTC liquidity : 13.49
WETH liquidity : 313.23
Liquidity status of other ticks are different too.
I can find ticks data is correct.
So I think I need to change mockTicks in calculateLockedLiqudity, but I couldn't get it to run properly because the modification didn't work well.
How can I get the exact liquidity token amount for the tick?