From 05ed18649f66123686df16bbe51f3c5aeacd7075 Mon Sep 17 00:00:00 2001 From: Apeguru Date: Sat, 6 Apr 2024 14:21:46 -0300 Subject: [PATCH] feat: Add user active unstaked liquidity TVL --- adapters/lynex/src/index.ts | 245 ++++++++++++------ adapters/lynex/src/sdk/config.ts | 32 +-- adapters/lynex/src/sdk/lensDetails.ts | 13 +- adapters/lynex/src/sdk/pools.ts | 286 ++++++++++++++++++++++ adapters/lynex/src/sdk/subgraphDetails.ts | 10 +- adapters/lynex/src/sdk/types.ts | 26 ++ 6 files changed, 498 insertions(+), 114 deletions(-) create mode 100644 adapters/lynex/src/sdk/pools.ts create mode 100644 adapters/lynex/src/sdk/types.ts diff --git a/adapters/lynex/src/index.ts b/adapters/lynex/src/index.ts index d43de8fe..c3e3873f 100644 --- a/adapters/lynex/src/index.ts +++ b/adapters/lynex/src/index.ts @@ -7,102 +7,193 @@ import { fetchUserVotes, } from "./sdk/lensDetails"; import BigNumber from "bignumber.js"; - -interface CSVRow { - block_number: string; - timestamp: string; - user_address: string; - token_address: string; - token_balance: string; -} +import { BlockData, OutputSchemaRow } from "./sdk/types"; +import { + getV2UserPositionsAtBlock, + getV3UserPositionsAtBlock, +} from "./sdk/pools"; const getData = async () => { - const snapshotBlocks = [2999728]; + const snapshotBlocks = [3460121]; - const csvRows: CSVRow[] = []; + const csvRows: OutputSchemaRow[] = []; for (let block of snapshotBlocks) { - const [userAddresses] = await Promise.all([getUserAddresses(block)]); - console.log(`Block: ${block}`); - console.log("UserAddresses: ", userAddresses.length); + const timestamp = await getTimestampAtBlock(block); + csvRows.push( + ...(await getUserTVLByBlock({ + blockNumber: block, + blockTimestamp: timestamp, + })) + ); + } - const tokenBalanceMap = {} as { - [userAddress: string]: { [tokenAddress: string]: BigNumber }; - }; + const ws = fs.createWriteStream("outputData.csv"); + write(csvRows, { headers: true }) + .pipe(ws) + .on("finish", () => { + console.log("CSV file has been written."); + }); +}; - const userPoolFetch = []; - const userVotesFetch = []; +export const getUserTVLByBlock = async ({ + blockNumber, + blockTimestamp, +}: BlockData): Promise => { + const result: OutputSchemaRow[] = []; + + const [stakedTvl, liquidityTvl] = await Promise.all([ + getUserStakedTVLByBlock({ blockNumber, blockTimestamp }), + getUserLiquidityTVLByBlock({ blockNumber, blockTimestamp }), + ]); + + // combine staked & unstaked + const combinedPositions = [...stakedTvl, ...liquidityTvl]; + const balances: Record> = {}; + for (const position of combinedPositions) { + balances[position.user_address] = balances[position.user_address] || {}; + + if (position.token_balance > 0n) + balances[position.user_address][position.token_address] = + (balances?.[position.user_address]?.[position.token_address] ?? 0n) + + position.token_balance; + } - for (const user of userAddresses) { - userPoolFetch.push(fetchUserPools(BigInt(block), user.id, user.pools)); - userVotesFetch.push(fetchUserVotes(BigInt(block), user.id)); + for (const [user, tokenBalances] of Object.entries(balances)) { + for (const [token, balance] of Object.entries(tokenBalances)) { + result.push({ + block_number: blockNumber, + timestamp: blockTimestamp, + user_address: user, + token_address: token, + token_balance: balance, + }); } + } - const userFetchResult = await Promise.all(userPoolFetch); - const userVotesResult = await Promise.all(userVotesFetch); - const block_number = block.toString(); - const timestamp = new Date(await getTimestampAtBlock(block)).toISOString(); - - for (const userFetchedPools of userFetchResult) { - for (const userPool of userFetchedPools) { - const user_address = userPool.result.userAddress.toLowerCase(); - const totalLPBalance = - userPool.result.account_lp_balance + - userPool.result.account_gauge_balance; - const total0 = - (totalLPBalance * userPool.result.reserve0) / - userPool.result.total_supply; - const total1 = - (totalLPBalance * userPool.result.reserve1) / - userPool.result.total_supply; - const token0Address = userPool.result.token0.toLowerCase(); - const token1Address = userPool.result.token1.toLowerCase(); - - // Aggregate tokens - tokenBalanceMap[user_address] = tokenBalanceMap[user_address] ?? {}; - tokenBalanceMap[user_address][token0Address] = BigNumber( - tokenBalanceMap[user_address][token0Address] ?? 0 - ).plus(total0.toString()); - tokenBalanceMap[user_address] = tokenBalanceMap[user_address] ?? {}; - tokenBalanceMap[user_address][token1Address] = BigNumber( - tokenBalanceMap[user_address][token1Address] ?? 0 - ).plus(total1.toString()); - } + return result; +}; + +export const getUserStakedTVLByBlock = async ({ + blockNumber, + blockTimestamp, +}: BlockData): Promise => { + const result: OutputSchemaRow[] = []; + const [userAddresses] = await Promise.all([getUserAddresses(blockNumber)]); + console.log(`Block: ${blockNumber}`); + console.log("UserAddresses: ", userAddresses.length); + + const tokenBalanceMap = {} as { + [userAddress: string]: { [tokenAddress: string]: BigNumber }; + }; + + const userPoolFetch = []; + const userVotesFetch = []; + + for (const user of userAddresses) { + userPoolFetch.push( + fetchUserPools(BigInt(blockNumber), user.id, user.pools) + ); + userVotesFetch.push(fetchUserVotes(BigInt(blockNumber), user.id)); + } + + const userFetchResult = await Promise.all(userPoolFetch); + const userVotesResult = await Promise.all(userVotesFetch); + + for (const userFetchedPools of userFetchResult) { + for (const userPool of userFetchedPools) { + const user_address = userPool.result.userAddress.toLowerCase(); + const totalLPBalance = userPool.result.account_gauge_balance; + const total0 = + (totalLPBalance * userPool.result.reserve0) / + userPool.result.total_supply; + const total1 = + (totalLPBalance * userPool.result.reserve1) / + userPool.result.total_supply; + const token0Address = userPool.result.token0.toLowerCase(); + const token1Address = userPool.result.token1.toLowerCase(); + + // Aggregate tokens + tokenBalanceMap[user_address] = tokenBalanceMap[user_address] ?? {}; + tokenBalanceMap[user_address][token0Address] = BigNumber( + tokenBalanceMap[user_address][token0Address] ?? 0 + ).plus(total0.toString()); + tokenBalanceMap[user_address] = tokenBalanceMap[user_address] ?? {}; + tokenBalanceMap[user_address][token1Address] = BigNumber( + tokenBalanceMap[user_address][token1Address] ?? 0 + ).plus(total1.toString()); } + } - for (const userFecthedVotes of userVotesResult) { - for (const userVote of userFecthedVotes) { - const user_address = userVote.result.userAddress.toLowerCase(); - const token0Address = VE_LYNX_ADDRESS.toLowerCase(); - tokenBalanceMap[user_address] = tokenBalanceMap[user_address] ?? {}; - tokenBalanceMap[user_address][token0Address] = BigNumber( - tokenBalanceMap[user_address][token0Address] ?? 0 - ).plus(userVote.result.amount.toString()); - } + for (const userFecthedVotes of userVotesResult) { + for (const userVote of userFecthedVotes) { + const user_address = userVote.result.userAddress.toLowerCase(); + const token0Address = VE_LYNX_ADDRESS.toLowerCase(); + tokenBalanceMap[user_address] = tokenBalanceMap[user_address] ?? {}; + tokenBalanceMap[user_address][token0Address] = BigNumber( + tokenBalanceMap[user_address][token0Address] ?? 0 + ).plus(userVote.result.amount.toString()); } + } - Object.entries(tokenBalanceMap).forEach(([user_address, balances]) => { - Object.entries(balances).forEach(([token_address, token_balance]) => { - if (token_balance.dp(0).lte(0)) { - return; - } - csvRows.push({ - block_number, - timestamp, - user_address, - token_address, - token_balance: token_balance.dp(0).toString(), - }); + Object.entries(tokenBalanceMap).forEach(([user_address, balances]) => { + Object.entries(balances).forEach(([token_address, token_balance]) => { + if (token_balance.dp(0).lte(0)) { + return; + } + result.push({ + block_number: blockNumber, + timestamp: blockTimestamp, + user_address, + token_address, + token_balance: BigInt(token_balance.dp(0).toNumber()), }); }); + }); + return result; +}; - const ws = fs.createWriteStream("outputData.csv"); - write(csvRows, { headers: true }) - .pipe(ws) - .on("finish", () => { - console.log("CSV file has been written."); +export const getUserLiquidityTVLByBlock = async ({ + blockNumber, + blockTimestamp, +}: BlockData): Promise => { + const result: OutputSchemaRow[] = []; + + const [v2Positions, v3Positions] = await Promise.all([ + getV2UserPositionsAtBlock(blockNumber), + getV3UserPositionsAtBlock(blockNumber), + ]); + + // combine v2 & v3 + const combinedPositions = [...v2Positions, ...v3Positions]; + const balances: Record> = {}; + for (const position of combinedPositions) { + balances[position.user] = balances[position.user] || {}; + + if (position.token0.balance > 0n) + balances[position.user][position.token0.address] = + (balances?.[position.user]?.[position.token0.address] ?? 0n) + + position.token0.balance; + + if (position.token1.balance > 0n) + balances[position.user][position.token1.address] = + (balances?.[position.user]?.[position.token1.address] ?? 0n) + + position.token1.balance; + } + + for (const [user, tokenBalances] of Object.entries(balances)) { + for (const [token, balance] of Object.entries(tokenBalances)) { + result.push({ + block_number: blockNumber, + timestamp: blockTimestamp, + user_address: user, + token_address: token, + token_balance: balance, }); + } } + + return result; }; getData().then(() => { diff --git a/adapters/lynex/src/sdk/config.ts b/adapters/lynex/src/sdk/config.ts index 418afb85..9232aef5 100644 --- a/adapters/lynex/src/sdk/config.ts +++ b/adapters/lynex/src/sdk/config.ts @@ -1,22 +1,14 @@ -export const enum CHAINS { - L2_CHAIN_ID = 59144, -} -export const enum PROTOCOLS { - LYNEX = 0, -} +import { createPublicClient, http } from "viem"; +import { linea } from "viem/chains"; -export const enum AMM_TYPES { - LYNEX = 0, -} +export const V2_SUBGRAPH_URL = + "https://api.studio.thegraph.com/query/59052/lynex-v1/version/latest"; +export const GAUGE_SUBGRAPH_URL = + "https://api.goldsky.com/api/public/project_cltyhthusbmxp01s95k9l8a1u/subgraphs/lynex-gauges/1.1.0/gn"; +export const V3_SUBGRAPH_URL = + "https://api.studio.thegraph.com/query/59052/lynex-cl/v0.0.1"; -export const SUBGRAPH_URLS = { - [CHAINS.L2_CHAIN_ID]: { - [PROTOCOLS.LYNEX]: { - [AMM_TYPES.LYNEX]: - "https://api.goldsky.com/api/public/project_cltyhthusbmxp01s95k9l8a1u/subgraphs/lynex-gauges/1.1.0/gn", - }, - }, -}; -export const RPC_URLS = { - [CHAINS.L2_CHAIN_ID]: "https://rpc.linea.build", -}; +export const client = createPublicClient({ + chain: linea, + transport: http("https://rpc.linea.build"), +}); diff --git a/adapters/lynex/src/sdk/lensDetails.ts b/adapters/lynex/src/sdk/lensDetails.ts index 005569f0..3cdf1096 100644 --- a/adapters/lynex/src/sdk/lensDetails.ts +++ b/adapters/lynex/src/sdk/lensDetails.ts @@ -7,8 +7,7 @@ import { MulticallParameters, PublicClient, } from "viem"; -import { linea } from "viem/chains"; -import { CHAINS, RPC_URLS } from "./config"; +import { client } from "./config"; import lensABI from "./abis/PairAPIABI.json"; import veLYNXAbi from "./abis/veLYNX.json"; @@ -67,10 +66,7 @@ export const fetchUserPools = async ( userAddress: string, userPools: string[] ): Promise => { - const publicClient = createPublicClient({ - chain: extractChain({ chains: [linea], id: CHAINS.L2_CHAIN_ID }), - transport: http(RPC_URLS[CHAINS.L2_CHAIN_ID]), - }); + const publicClient = client; const calls = userPools.map((pool: string) => { return { @@ -95,10 +91,7 @@ export const fetchUserVotes = async ( blockNumber: bigint, userAddress: string ): Promise => { - const publicClient = createPublicClient({ - chain: extractChain({ chains: [linea], id: CHAINS.L2_CHAIN_ID }), - transport: http(RPC_URLS[CHAINS.L2_CHAIN_ID]), - }); + const publicClient = client; const userBalanceCall = await multicall( publicClient, diff --git a/adapters/lynex/src/sdk/pools.ts b/adapters/lynex/src/sdk/pools.ts new file mode 100644 index 00000000..60d3fd31 --- /dev/null +++ b/adapters/lynex/src/sdk/pools.ts @@ -0,0 +1,286 @@ +import BigNumber from "bignumber.js"; +import { V2_SUBGRAPH_URL, V3_SUBGRAPH_URL, client } from "./config"; +import { UserPosition } from "./types"; + +type V2Position = { + liquidityTokenBalance: string; + user: { + id: string; + }; + pair: { + totalSupply: string; + reserve0: string; + reserve1: string; + token0: { + id: string; + symbol: string; + decimals: number; + }; + token1: { + id: string; + symbol: string; + decimals: number; + }; + token0Price: string; + token1Price: string; + }; +}; + +export const fromWei = (number: number | string, decimals = 18) => + new BigNumber(number).div(new BigNumber(10).pow(decimals)); +export const toWei = (number: number | string, decimals = 18) => + new BigNumber(number).times(new BigNumber(10).pow(decimals)); + +const getV2PositionReserves = (position: V2Position) => { + return { + reserve0: BigInt( + toWei(position.pair.reserve0, position.pair.token0.decimals) + .times(toWei(position.liquidityTokenBalance)) + .div(toWei(position.pair.totalSupply)) + .dp(0) + .toNumber() + ), + reserve1: BigInt( + toWei(position.pair.reserve1, position.pair.token1.decimals) + .times(toWei(position.liquidityTokenBalance)) + .div(toWei(position.pair.totalSupply)) + .dp(0) + .toNumber() + ), + }; +}; + +export const getV2UserPositionsAtBlock = async ( + blockNumber: number +): Promise => { + const result: UserPosition[] = []; + + let skip = 0; + let fetchNext = true; + while (fetchNext) { + const query = `query { + liquidityPositions( + first: 1000, + skip: ${skip}, + where: { liquidityTokenBalance_gt: 0 }, + block: { number: ${blockNumber} } + ) { + liquidityTokenBalance + user { + id + } + pair { + totalSupply + reserve0 + reserve1 + token0 { + id + symbol + decimals + } + token1 { + id + symbol + decimals + } + token0Price + token1Price + } + } + }`; + + const response = await fetch(V2_SUBGRAPH_URL, { + method: "POST", + body: JSON.stringify({ query }), + headers: { "Content-Type": "application/json" }, + }); + const { + data: { liquidityPositions }, + } = await response.json(); + + result.push( + ...liquidityPositions.map((position: V2Position) => { + const { reserve0, reserve1 } = getV2PositionReserves(position); + return { + user: position.user.id, + token0: { + address: position.pair.token0.id, + balance: reserve0, + symbol: position.pair.token0.symbol, + }, + token1: { + address: position.pair.token1.id, + balance: reserve1, + symbol: position.pair.token1.symbol, + }, + }; + }) + ); + + if (liquidityPositions.length < 1000) { + fetchNext = false; + } else { + skip += 1000; + } + } + + return result; +}; + +type V3Position = { + liquidity: string; + owner: string; + pool: { + sqrtPrice: string; + tick: string; + token0: { + id: string; + symbol: string; + }; + token1: { + id: string; + symbol: string; + }; + token0Price: string; + token1Price: string; + }; + tickLower: { + tickIdx: string; + }; + tickUpper: { + tickIdx: string; + }; +}; + +const getV3PositionReserves = (position: V3Position) => { + const liquidity = +position.liquidity; + const _sqrtPrice = +position.pool.sqrtPrice; + const currentTick = +position.pool.tick; + const tickLower = +position.tickLower.tickIdx; + const tickUpper = +position.tickUpper.tickIdx; + + let reserve0 = 0n; + let reserve1 = 0n; + + if (liquidity === 0) { + return { + reserve0, + reserve1, + }; + } + + const sqrtRatioA = Math.sqrt(1.0001 ** tickLower); + const sqrtRatioB = Math.sqrt(1.0001 ** tickUpper); + const sqrtPrice = _sqrtPrice / 2 ** 96; + + // Onlye return active TVL + if (currentTick < tickLower) { + reserve0 = BigInt( + Math.floor( + liquidity * ((sqrtRatioB - sqrtRatioA) / (sqrtRatioA * sqrtRatioB)) + ) + ); + } else if (currentTick >= tickUpper) { + reserve1 = BigInt(Math.floor(liquidity * (sqrtRatioB - sqrtRatioA))); + } else if (currentTick >= tickLower && currentTick < tickUpper) { + reserve0 = BigInt( + Math.floor( + liquidity * ((sqrtRatioB - sqrtPrice) / (sqrtPrice * sqrtRatioB)) + ) + ); + reserve1 = BigInt(Math.floor(liquidity * (sqrtPrice - sqrtRatioA))); + } + + return { + reserve0, + reserve1, + }; +}; + +export const getV3UserPositionsAtBlock = async ( + blockNumber: number +): Promise => { + const result: UserPosition[] = []; + + let skip = 0; + let fetchNext = true; + while (fetchNext) { + const query = `query { + positions( + first: 1000, + skip: ${skip}, + where: { liquidity_gt: 0 }, + block: { number: ${blockNumber} } + ) { + liquidity + owner + pool { + sqrtPrice + tick + token0 { + id + symbol + } + token1 { + id + symbol + } + token0Price + token1Price + } + tickLower { + tickIdx + } + tickUpper { + tickIdx + } + } + }`; + + const response = await fetch(V3_SUBGRAPH_URL, { + method: "POST", + body: JSON.stringify({ query }), + headers: { "Content-Type": "application/json" }, + }); + + const { + data: { positions }, + } = await response.json(); + + result.push( + ...positions.map((position: V3Position) => { + const { reserve0, reserve1 } = getV3PositionReserves(position); + return { + user: position.owner, + token0: { + address: position.pool.token0.id, + balance: reserve0, + symbol: position.pool.token0.symbol, + usdPrice: +position.pool.token0Price, + }, + token1: { + address: position.pool.token1.id, + balance: reserve1, + symbol: position.pool.token1.symbol, + usdPrice: +position.pool.token1Price, + }, + }; + }) + ); + + if (positions.length < 1000) { + fetchNext = false; + } else { + skip += 1000; + } + } + + return result; +}; + +export const getTimestampAtBlock = async (blockNumber: number) => { + const block = await client.getBlock({ + blockNumber: BigInt(blockNumber), + }); + return Number(block.timestamp * 1000n); +}; diff --git a/adapters/lynex/src/sdk/subgraphDetails.ts b/adapters/lynex/src/sdk/subgraphDetails.ts index 0ea70c75..a0e76c95 100644 --- a/adapters/lynex/src/sdk/subgraphDetails.ts +++ b/adapters/lynex/src/sdk/subgraphDetails.ts @@ -1,6 +1,6 @@ import { createPublicClient, extractChain, http } from "viem"; import { linea } from "viem/chains"; -import { CHAINS, PROTOCOLS, RPC_URLS, SUBGRAPH_URLS } from "./config"; +import { GAUGE_SUBGRAPH_URL, client } from "./config"; interface UserStake { id: string; @@ -10,8 +10,7 @@ interface UserStake { export const getUserAddresses = async ( blockNumber: number ): Promise => { - let subgraphUrl = - SUBGRAPH_URLS[CHAINS.L2_CHAIN_ID][PROTOCOLS.LYNEX][PROTOCOLS.LYNEX]; + let subgraphUrl = GAUGE_SUBGRAPH_URL; let blockQuery = blockNumber !== 0 ? ` block: {number: ${blockNumber}}` : ``; let skip = 0; @@ -70,10 +69,7 @@ export const getUserAddresses = async ( }; export const getTimestampAtBlock = async (blockNumber: number) => { - const publicClient = createPublicClient({ - chain: extractChain({ chains: [linea], id: CHAINS.L2_CHAIN_ID }), - transport: http(RPC_URLS[CHAINS.L2_CHAIN_ID]), - }); + const publicClient = client; const block = await publicClient.getBlock({ blockNumber: BigInt(blockNumber), diff --git a/adapters/lynex/src/sdk/types.ts b/adapters/lynex/src/sdk/types.ts new file mode 100644 index 00000000..2d822b65 --- /dev/null +++ b/adapters/lynex/src/sdk/types.ts @@ -0,0 +1,26 @@ +export interface BlockData { + blockNumber: number; + blockTimestamp: number; +} + +export type OutputSchemaRow = { + block_number: number; + timestamp: number; + user_address: string; + token_address: string; + token_balance: bigint; + token_symbol?: string; + usd_price?: number; +}; + +export type UserPosition = { + user: string; + token0: { + address: string; + balance: bigint; + }; + token1: { + address: string; + balance: bigint; + }; +};