From 12861793dca9385951a2e224e10d8a3d079d9fd8 Mon Sep 17 00:00:00 2001 From: jinoosss Date: Mon, 4 Dec 2023 15:43:15 +0900 Subject: [PATCH] feat: [GSW-463] Calculate the Price Range of Liquidity --- .../common/pool-graph/PoolGraph.tsx | 27 ++------ .../PoolAddLiquidityContainer.tsx | 2 +- packages/web/src/hooks/pool/use-pool.tsx | 2 +- .../web/src/hooks/pool/use-select-pool.tsx | 31 ++++----- .../src/models/pool/mapper/pool-rpc-mapper.ts | 51 ++++++++++++++- .../src/models/pool/pool-detail-rpc-model.ts | 6 ++ .../models/pool/position-detail-rpc-model.ts | 5 ++ .../repositories/pool/pool-repository-impl.ts | 7 +- .../repositories/pool/pool-repository-mock.ts | 3 +- .../src/repositories/pool/pool-repository.ts | 3 +- packages/web/src/utils/swap-utils.spec.ts | 64 +++++-------------- packages/web/src/utils/swap-utils.ts | 18 +++--- 12 files changed, 120 insertions(+), 99 deletions(-) create mode 100644 packages/web/src/models/pool/pool-detail-rpc-model.ts create mode 100644 packages/web/src/models/pool/position-detail-rpc-model.ts diff --git a/packages/web/src/components/common/pool-graph/PoolGraph.tsx b/packages/web/src/components/common/pool-graph/PoolGraph.tsx index 194cbc443..3aaa0e9cf 100644 --- a/packages/web/src/components/common/pool-graph/PoolGraph.tsx +++ b/packages/web/src/components/common/pool-graph/PoolGraph.tsx @@ -100,7 +100,7 @@ const PoolGraph: React.FC = ({ /** Zoom */ const zoom: d3.ZoomBehavior = d3 .zoom() - .scaleExtent([1, tickFullRange / 2]) + .scaleExtent([0, tickFullRange]) .translateExtent([ [0, 0], [boundsWidth, boundsHeight] @@ -115,10 +115,11 @@ const PoolGraph: React.FC = ({ const svgElement = d3.select(svgRef.current); const minXTick = minX || 0; const maxXTick = maxX || 0; - const distance = Math.abs(centerX - minXTick) > Math.abs(centerX - maxXTick) - ? Math.abs(minXTick - centerX) - : Math.abs(maxXTick - centerX); - const scaleRate = (tickFullRange / (distance) / 2); + const tick = currentTick || 0; + const distance = Math.abs(tick - minXTick) > Math.abs(tick - maxXTick) + ? Math.abs(tick - minXTick) + : Math.abs(tick - maxXTick); + const scaleRate = (MAX_TICK / distance); zoom.scaleTo(svgElement, scaleRate, [scaleX(centerX), 0]); } @@ -130,20 +131,6 @@ const PoolGraph: React.FC = ({ updateChart(); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function zoomIn() { - d3.select(svgRef.current) - .transition() - .call(zoom.scaleBy, 4); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function zoomOut() { - d3.select(svgRef.current) - .transition() - .call(zoom.scaleBy, 0.25); - } - function getTickSpacing() { if (bins.length < 1) { return 0; @@ -151,7 +138,7 @@ const PoolGraph: React.FC = ({ if (bins.length === 2) { return 20; } - const spacing = scaleX(bins[1].minTick) - scaleX(bins[0].minTick); + const spacing = scaleX(bins[0].maxTick) - scaleX(bins[0].minTick); if (spacing < 2) { return spacing; } diff --git a/packages/web/src/containers/pool-add-liquidity-container/PoolAddLiquidityContainer.tsx b/packages/web/src/containers/pool-add-liquidity-container/PoolAddLiquidityContainer.tsx index 91a2a222a..87095a2f6 100644 --- a/packages/web/src/containers/pool-add-liquidity-container/PoolAddLiquidityContainer.tsx +++ b/packages/web/src/containers/pool-add-liquidity-container/PoolAddLiquidityContainer.tsx @@ -381,7 +381,7 @@ const EarnAddLiquidityContainer: React.FC = () => { currentTick={null} submitType={submitType} submit={submit} - isEarnAdd={true} + isEarnAdd={false} connected={connectedWallet} slippage={slippage} changeSlippage={handleChangeSlippage} diff --git a/packages/web/src/hooks/pool/use-pool.tsx b/packages/web/src/hooks/pool/use-pool.tsx index 7dd8d4ddd..05a3aa289 100644 --- a/packages/web/src/hooks/pool/use-pool.tsx +++ b/packages/web/src/hooks/pool/use-pool.tsx @@ -53,7 +53,7 @@ export const usePool = ({ }, [compareToken, tokenA, tokenB]); async function fetchPoolInfos(pools: PoolModel[]) { - const poolInfos = await (await Promise.all(pools.map(pool => poolRepository.getPoolInfoByPoolPath(pool.path).catch(null)))).filter(info => info !== null); + const poolInfos = await (await Promise.all(pools.map(pool => poolRepository.getPoolDetailRPCByPoolPath(pool.path).catch(null)))).filter(info => info !== null); return poolInfos; } diff --git a/packages/web/src/hooks/pool/use-select-pool.tsx b/packages/web/src/hooks/pool/use-select-pool.tsx index 5cb0f3b2b..8de3bddf9 100644 --- a/packages/web/src/hooks/pool/use-select-pool.tsx +++ b/packages/web/src/hooks/pool/use-select-pool.tsx @@ -2,9 +2,9 @@ import { SwapFeeTierInfoMap, SwapFeeTierType } from "@constants/option.constant" import { TokenModel } from "@models/token/token-model"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useGnoswapContext } from "@hooks/common/use-gnoswap-context"; -import { PoolInfoModel } from "@models/pool/pool-info-model"; import { feeBoostRateByPrices, priceToNearTick, tickToPrice } from "@utils/swap-utils"; -import { MAX_TICK, MIN_PRICE, MIN_TICK } from "@constants/swap.constant"; +import { PoolDetailRPCModel } from "@models/pool/pool-detail-rpc-model"; +import { MAX_TICK, MIN_PRICE_X96, MIN_TICK } from "@constants/swap.constant"; type RenderState = "NONE" | "CREATE" | "LOADING" | "DONE"; @@ -66,7 +66,7 @@ export const useSelectPool = ({ const [minPosition, setMinPosition] = useState(null); const [maxPosition, setMaxPosition] = useState(null); const [compareToken, setCompareToken] = useState(tokenA); - const [poolInfo, setPoolInfo] = useState(null); + const [poolInfo, setPoolInfo] = useState(null); const [interactionType, setInteractionType] = useState<"NONE" | "INTERACTION" | "TICK_UPDATE" | "FINISH">("NONE"); const renderState: RenderState = useMemo(() => { @@ -136,7 +136,7 @@ export const useSelectPool = ({ } const logMin = minPrice <= 0 ? - Math.log(currentPrice / MIN_PRICE) : + Math.log(currentPrice / Number(MIN_PRICE_X96)) : Math.log(currentPrice / minPrice); const logMax = Math.log(maxPrice / currentPrice); return logMin * 100 / (logMin + logMax); @@ -147,7 +147,7 @@ export const useSelectPool = ({ return null; } if (minPrice <= 0) { - return feeBoostRateByPrices(MIN_PRICE, maxPrice); + return feeBoostRateByPrices(Number(MIN_PRICE_X96), maxPrice); } return feeBoostRateByPrices(minPrice, maxPrice); }, [maxPrice, minPrice]); @@ -273,32 +273,33 @@ export const useSelectPool = ({ setPoolInfo(null); return; } - const poolInfo: PoolInfoModel = { + const poolInfo: PoolDetailRPCModel = { poolPath: "", - tokenABalance: 0, - tokenBBalance: 0, + tokenAPath: "", + tokenBPath: "", + fee: 0, + tokenABalance: 0n, + tokenBBalance: 0n, tickSpacing: SwapFeeTierInfoMap[feeTier].tickSpacing, maxLiquidityPerTick: 0, price: startPrice, - sqrtPriceX96: 0, + sqrtPriceX96: 0n, tick: 0, feeProtocol: 0, - feeGrowthGlobal0X128: 0, - feeGrowthGlobal1X128: 0, tokenAProtocolFee: 0, tokenBProtocolFee: 0, - liquidity: 0, + liquidity: 0n, ticks: [], tickBitmaps: [], positions: [] }; setPoolInfo(poolInfo); } else { - const tokenPair = [tokenA.symbol.toLowerCase(), tokenB.symbol.toLowerCase()].sort(); - const poolPath = `${tokenPair.join("_")}_${SwapFeeTierInfoMap[feeTier].fee}`; + const tokenPair = [tokenA.path, tokenB.path].sort(); + const poolPath = `${tokenPair.join(":")}:${SwapFeeTierInfoMap[feeTier].fee}`; const reverse = [tokenA?.path, tokenB?.path].sort().findIndex(path => path === compareToken?.path) === 1; - poolRepository.getPoolInfoByPoolPath(poolPath).then(poolInfo => { + poolRepository.getPoolDetailRPCByPoolPath(poolPath).then(poolInfo => { const changedPoolInfo = reverse === false ? poolInfo : { ...poolInfo, price: 1 / poolInfo.price, diff --git a/packages/web/src/models/pool/mapper/pool-rpc-mapper.ts b/packages/web/src/models/pool/mapper/pool-rpc-mapper.ts index cbe37ecfd..ebb8bd179 100644 --- a/packages/web/src/models/pool/mapper/pool-rpc-mapper.ts +++ b/packages/web/src/models/pool/mapper/pool-rpc-mapper.ts @@ -1,5 +1,6 @@ import { PoolRPCResponse } from "@repositories/pool/response/pool-rpc-response"; import { rawBySqrtX96 } from "@utils/swap-utils"; +import { PoolDetailRPCModel } from "../pool-detail-rpc-model"; import { PoolRPCModel } from "../pool-rpc-model"; export class PoolRPCMapper { @@ -32,16 +33,64 @@ export class PoolRPCMapper { return acc; }, {}), positions: responseData.positions.map(position => ({ - liquidity: BigInt(position.liquidity), owner: position.owner, tickLower: position.tick_lower, tickUpper: position.tick_upper, + liquidity: BigInt(position.liquidity), tokenAOwed: BigInt(position.token0_owed), tokenBOwed: BigInt(position.token1_owed), })), }; } + public static detailFrom(data: PoolRPCResponse): PoolDetailRPCModel { + const responseData = data; + const sqrtPriceX96 = BigInt(responseData.sqrt_price_x96); + const price = rawBySqrtX96(sqrtPriceX96); + const tickSpacing = responseData.tick_spacing; + console.log(data); + + return { + poolPath: responseData.pool_path, + tokenAPath: responseData.token0_path, + tokenBPath: responseData.token1_path, + fee: responseData.fee, + tokenABalance: BigInt(responseData.token0_balance), + tokenBBalance: BigInt(responseData.token1_balance), + tickSpacing, + maxLiquidityPerTick: responseData.max_liquidity_per_tick, + price, + sqrtPriceX96, + tick: responseData.tick, + feeProtocol: responseData.fee_protocol, + tokenAProtocolFee: responseData.token0_protocol_fee, + tokenBProtocolFee: responseData.token1_protocol_fee, + liquidity: BigInt(responseData.liquidity), + ticks: responseData.ticks, + tickBitmaps: Object.entries(responseData.tick_bitmaps).reduce< + { [key in string]: string } + >((acc, [key, value]) => { + acc[key] = value.toString(); + return acc; + }, {}), + positions: data.positions.map(position => { + const tickLower = position.tick_lower; + const tickUpper = position.tick_upper; + const tickCount = 1 + (tickUpper - tickLower) / tickSpacing; + const liquidityOfTick = Number(position.liquidity) / tickCount; + return { + owner: position.owner, + tickLower, + tickUpper, + liquidityOfTick, + liquidity: BigInt(position.liquidity), + tokenAOwed: BigInt(position.token0_owed), + tokenBOwed: BigInt(position.token1_owed), + }; + }), + }; + } + public static fromList(response: PoolRPCResponse[] | null): PoolRPCModel[] { if (!response) { throw new Error("mapper error"); diff --git a/packages/web/src/models/pool/pool-detail-rpc-model.ts b/packages/web/src/models/pool/pool-detail-rpc-model.ts new file mode 100644 index 000000000..152518a64 --- /dev/null +++ b/packages/web/src/models/pool/pool-detail-rpc-model.ts @@ -0,0 +1,6 @@ +import { PoolRPCModel } from "./pool-rpc-model"; +import { PositionDetailRPCModel } from "./position-detail-rpc-model"; + +export interface PoolDetailRPCModel extends PoolRPCModel { + positions: PositionDetailRPCModel[]; +} diff --git a/packages/web/src/models/pool/position-detail-rpc-model.ts b/packages/web/src/models/pool/position-detail-rpc-model.ts new file mode 100644 index 000000000..248997a2f --- /dev/null +++ b/packages/web/src/models/pool/position-detail-rpc-model.ts @@ -0,0 +1,5 @@ +import { PositionRPCModel } from "./position-rpc-model"; + +export interface PositionDetailRPCModel extends PositionRPCModel { + liquidityOfTick: number; +} diff --git a/packages/web/src/repositories/pool/pool-repository-impl.ts b/packages/web/src/repositories/pool/pool-repository-impl.ts index 158e9a30d..7b70e8050 100644 --- a/packages/web/src/repositories/pool/pool-repository-impl.ts +++ b/packages/web/src/repositories/pool/pool-repository-impl.ts @@ -28,6 +28,7 @@ import { AddLiquidityRequest } from "./request/add-liquidity-request"; import BigNumber from "bignumber.js"; import { priceToNearTick, tickToPrice } from "@utils/swap-utils"; import { X96 } from "@constants/swap.constant"; +import { PoolDetailRPCModel } from "@models/pool/pool-detail-rpc-model"; const POOL_PATH = process.env.NEXT_PUBLIC_PACKAGE_POOL_PATH || ""; const POSITION_PATH = process.env.NEXT_PUBLIC_PACKAGE_POSITION_PATH || ""; @@ -84,9 +85,9 @@ export class PoolRepositoryImpl implements PoolRepository { return response.data; }; - getPoolInfoByPoolPath = async ( + getPoolDetailRPCByPoolPath = async ( poolPath: string, - ): Promise => { + ): Promise => { const poolPackagePath = process.env.NEXT_PUBLIC_PACKAGE_POOL_PATH; if (!poolPackagePath || !this.rpcProvider) { throw new CommonError("FAILED_INITIALIZE_ENVIRONMENT"); @@ -105,7 +106,7 @@ export class PoolRepositoryImpl implements PoolRepository { if (!response?.response?.data) { return null; } - return PoolRPCMapper.from(response?.response?.data); + return PoolRPCMapper.detailFrom(response?.response?.data); }) .catch(e => { console.error(e); diff --git a/packages/web/src/repositories/pool/pool-repository-mock.ts b/packages/web/src/repositories/pool/pool-repository-mock.ts index b64482227..f3e7a2b34 100644 --- a/packages/web/src/repositories/pool/pool-repository-mock.ts +++ b/packages/web/src/repositories/pool/pool-repository-mock.ts @@ -6,6 +6,7 @@ import { PoolError } from "@common/errors/pool"; import rpcPools from "./mock/rpc-pools.json"; import { PoolRPCMapper } from "@models/pool/mapper/pool-rpc-mapper"; import { PoolModel } from "@models/pool/pool-model"; +import { PoolDetailRPCModel } from "@models/pool/pool-detail-rpc-model"; export class PoolRepositoryMock implements PoolRepository { getPools = async (): Promise => { return []; @@ -15,7 +16,7 @@ export class PoolRepositoryMock implements PoolRepository { return rpcPools.map(pool => PoolRPCMapper.from(pool as any)); }; - getPoolInfoByPoolPath = async (): Promise => { + getPoolDetailRPCByPoolPath = async (): Promise => { throw new PoolError("NOT_FOUND_POOL"); }; diff --git a/packages/web/src/repositories/pool/pool-repository.ts b/packages/web/src/repositories/pool/pool-repository.ts index cbfbb2414..25f64a686 100644 --- a/packages/web/src/repositories/pool/pool-repository.ts +++ b/packages/web/src/repositories/pool/pool-repository.ts @@ -1,3 +1,4 @@ +import { PoolDetailRPCModel } from "@models/pool/pool-detail-rpc-model"; import { PoolModel } from "@models/pool/pool-model"; import { PoolRPCModel } from "@models/pool/pool-rpc-model"; import { AddLiquidityRequest } from "./request/add-liquidity-request"; @@ -9,7 +10,7 @@ export interface PoolRepository { getRPCPools: () => Promise; - getPoolInfoByPoolPath: (poolPath: string) => Promise; + getPoolDetailRPCByPoolPath: (poolPath: string) => Promise; getPoolDetailByPoolId: (poolId: string) => Promise; diff --git a/packages/web/src/utils/swap-utils.spec.ts b/packages/web/src/utils/swap-utils.spec.ts index 11c2fa4b4..d721f7758 100644 --- a/packages/web/src/utils/swap-utils.spec.ts +++ b/packages/web/src/utils/swap-utils.spec.ts @@ -9,23 +9,25 @@ import { describe("tick convert to price", () => { test("0 to 1", () => { const tick = 0; - console.log(1n << 96n); expect(tickToPrice(tick)).toBe(1); }); test("10000 to 2.718145926825225", () => { const tick = 10000; + const a = tickToPrice(tick); expect(tickToPrice(tick)).toBe(2.718145926825225); }); test("10001 to 2.7184177414179076", () => { const tick = 10001; + const a = tickToPrice(tick); expect(tickToPrice(tick)).toBe(2.7184177414179076); }); - test("100000 to 148.3760629230746", () => { + test("100000 to 22015.456048552198", () => { const tick = 100000; - expect(tickToPrice(tick)).toBe(148.3760629230746); + const a = tickToPrice(tick); + expect(tickToPrice(tick)).toBe(22015.456048552198); }); }); @@ -37,51 +39,19 @@ describe("tick convert to sqrtPriceX96", () => { ); }); - test("10000 to 1.6486800559311758", () => { - const tick = 10000; - expect(tickToPrice(tick)).toBe(1.6486800559311758); - }); - - test("10001 to 1.6487624878732252", () => { - const tick = 10001; - expect(tickToPrice(tick)).toBe(1.6487624878732252); - }); - - test("100000 to 148.3760629230746", () => { - const tick = 100000; - console.log(tickToPrice(tick)); - expect(tickToPrice(tick)).toBe(148.3760629230746); - }); - - test("100000 to 148.3760629230746", () => { - const tick = 100000; - console.log(tickToPrice(tick)); - expect(tickToPrice(tick)).toBe(148.3760629230746); - }); -}); - -describe("tick convert to sqrtPriceX96", () => { - test("-5120 to 61334630738499455555414115609", () => { - const tick = -5120; - expect(tickToSqrtPriceX96(tick).toString()).toBe( - "61334630738499455555414115609", - ); - }); - - test("10000 to 1.6486800559311758", () => { + test("10000 to 2.718145926825225", () => { const tick = 10000; - expect(tickToPrice(tick)).toBe(1.6486800559311758); + expect(tickToPrice(tick)).toBe(2.718145926825225); }); - test("10001 to 1.6487624878732252", () => { + test("10001 to 2.7184177414179076", () => { const tick = 10001; - expect(tickToPrice(tick)).toBe(1.6487624878732252); + expect(tickToPrice(tick)).toBe(2.7184177414179076); }); - test("100000 to 148.3760629230746", () => { + test("100000 to 22015.456048552198", () => { const tick = 100000; - console.log(tickToPrice(tick)); - expect(tickToPrice(tick)).toBe(148.3760629230746); + expect(tickToPrice(tick)).toBe(22015.456048552198); }); }); @@ -93,17 +63,17 @@ describe("price convert to tick", () => { test("1.6486800559311758 to 10000", () => { const price = 1.6486800559311758; - expect(priceToTick(price)).toBe(5000); + expect(priceToTick(price)).toBe(10000); }); test("1.6487624878732252 to 10001", () => { const price = 1.6487624878732252; - expect(priceToTick(price)).toBe(5001); + expect(priceToTick(price)).toBe(10001); }); test("0.60651549714 to -10001", () => { const price = 0.60651549714; - expect(priceToTick(price)).toBe(-5001); + expect(priceToTick(price)).toBe(-10001); }); }); @@ -117,19 +87,19 @@ describe("price convert to near tick", () => { test("1.6486800559311758 to 10002", () => { const price = 1.6489273641220126; const tickSpacing = 2; - expect(priceToNearTick(price, tickSpacing)).toBe(5002); + expect(priceToNearTick(price, tickSpacing)).toBe(10002); }); test("0.60651549714 to -10002", () => { const price = 0.60651549714; const tickSpacing = 2; - expect(priceToNearTick(price, tickSpacing)).toBe(-5002); + expect(priceToNearTick(price, tickSpacing)).toBe(-10002); }); test("0.60651549714 to -10004", () => { const price = 0.60651549714; const tickSpacing = 4; - expect(priceToNearTick(price, tickSpacing)).toBe(-5004); + expect(priceToNearTick(price, tickSpacing)).toBe(-10004); }); }); diff --git a/packages/web/src/utils/swap-utils.ts b/packages/web/src/utils/swap-utils.ts index da3a2779b..1fdc7d7ac 100644 --- a/packages/web/src/utils/swap-utils.ts +++ b/packages/web/src/utils/swap-utils.ts @@ -2,16 +2,12 @@ import { SwapFeeTierInfoMap, SwapFeeTierType, } from "@constants/option.constant"; -import { MAX_TICK, MIN_TICK, Q96, X96 } from "@constants/swap.constant"; +import { MAX_TICK, MIN_TICK, Q96 } from "@constants/swap.constant"; import BigNumber from "bignumber.js"; import { tickToSqrtPriceX96 } from "./math.utils"; const LOG10001 = Math.log(1.0001); -export function getCurrentPriceByRaw(raw: string) { - return BigNumber(raw).dividedBy(X96).pow(2); -} - export function makeSwapFeeTier(value: string | number): SwapFeeTierType { for (const swapFeeTierInfo of Object.values(SwapFeeTierInfoMap)) { if (swapFeeTierInfo.fee === Number(value)) { @@ -54,7 +50,10 @@ export function priceToNearTick(price: number | bigint, tickSpacing: number) { } export function rawBySqrtX96(value: number | bigint | string) { - return BigNumber(value.toString()).dividedBy(Q96.toString()).toNumber(); + return BigNumber(value.toString()) + .dividedBy(Q96.toString()) + .pow(2) + .toNumber(); } export function priceX96ToNearTick( @@ -96,13 +95,14 @@ export function feeBoostRateByPrices( } const sqrt4Value = BigNumber(minPrice / maxPrice) .squareRoot() - .squareRoot(); + .squareRoot() + .abs(); return BigNumber(1) .dividedBy(1 - sqrt4Value.toNumber()) .toFixed(2); } export function sqrtPriceX96ToTick(priceX96: number | BigInt) { - const price = getCurrentPriceByRaw(priceX96.toString()); - return priceToTick(price.toNumber()); + const price = rawBySqrtX96(priceX96.toString()); + return priceToTick(price); }