Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: sushiswap adapter #37

Merged
merged 2 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions adapters/sushiswap/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "sushiswap",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "commonjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node dist/index.js",
"compile": "tsc",
"watch": "tsc -w",
"clear": "rm -rf dist"
},
"keywords": [],
"author": "",
"license": "UNLICENSED",
"dependencies": {
"csv-parser": "^3.0.0",
"fast-csv": "^5.0.1",
"viem": "^2.8.13"
},
"devDependencies": {
"@types/node": "^20.11.17",
"typescript": "^5.3.3"
}
}
89 changes: 89 additions & 0 deletions adapters/sushiswap/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { promisify } from 'util';
import stream from 'stream';
import csv from 'csv-parser';
import fs from 'fs';
import { write } from 'fast-csv';

import { BlockData, OutputSchemaRow } from './sdk/types';
import { getTimestampAtBlock, getV2UserPositionsAtBlock, getV3UserPositionsAtBlock } from './sdk/lib';

const pipeline = promisify(stream.pipeline);

const readBlocksFromCSV = async (filePath: string): Promise<number[]> => {
const blocks: number[] = [];
await pipeline(
fs.createReadStream(filePath),
csv(),
async function* (source) {
for await (const chunk of source) {
// Assuming each row in the CSV has a column 'block' with the block number
if (chunk.block) blocks.push(parseInt(chunk.block, 10));
}
}
);
return blocks;
};

const getData = async () => {
const blocks = [
3203675
]; //await readBlocksFromCSV('src/sdk/mode_chain_daily_blocks.csv');

const csvRows: OutputSchemaRow[] = [];

for (const block of blocks) {
const timestamp = await getTimestampAtBlock(block)

csvRows.push(...await getUserTVLByBlock({ blockNumber: block, blockTimestamp: timestamp }))
}

// Write the CSV output to a file
const ws = fs.createWriteStream('outputData.csv');
write(csvRows, { headers: true }).pipe(ws).on('finish', () => {
console.log("CSV file has been written.");
});
};

export const getUserTVLByBlock = async ({ blockNumber, blockTimestamp }: BlockData): Promise<OutputSchemaRow[]> => {
const result: OutputSchemaRow[] = []

const [v2Positions, v3Positions] = await Promise.all([
getV2UserPositionsAtBlock(blockNumber),
getV3UserPositionsAtBlock(blockNumber)
])

// combine v2 & v3
const combinedPositions = [...v2Positions, ...v3Positions]
const balances: Record<string, Record<string, bigint>> = {}
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(() => {
console.log("Done");
});
10 changes: 10 additions & 0 deletions adapters/sushiswap/src/sdk/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createPublicClient, http } from "viem";
import { linea } from "viem/chains"

export const V2_SUBGRAPH_URL = "https://graph-query.linea.build/subgraphs/name/sushiswap/sushiswap-linea"
export const V3_SUBGRAPH_URL = "https://graph-query.linea.build/subgraphs/name/sushi-v3/v3-linea"

export const client = createPublicClient({
chain: linea,
transport: http("https://rpc.linea.build")
})
241 changes: 241 additions & 0 deletions adapters/sushiswap/src/sdk/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { V2_SUBGRAPH_URL, V3_SUBGRAPH_URL, client } from "./config"
import { UserPosition } from "./types"

type V2Position = {
balance: string,
user: {
id: string,
},
pair: {
liquidity: string,
reserve0: string,
reserve1: string,
token0: {
id: string,
symbol: string,
},
token1: {
id: string,
symbol: string,
}
token0Price: string,
token1Price: string
}
}

const getV2PositionReserves = (position: V2Position) => {
return {
reserve0: BigInt(position.pair.reserve0) * BigInt(position.balance) / BigInt(position.pair.liquidity),
reserve1: BigInt(position.pair.reserve1) * BigInt(position.balance) / BigInt(position.pair.liquidity)
}
}

export const getV2UserPositionsAtBlock = async (blockNumber: number): Promise<UserPosition[]> => {
const result: UserPosition[] = []

let skip = 0
let fetchNext = true
while (fetchNext) {
const query = `query {
liquidityPositions(
first: 1000,
skip: ${skip},
where: { balance_gt: 0 },
block: { number: ${blockNumber} }
) {
balance
user {
id
}
pair {
liquidity
reserve0
reserve1
token0 {
id
symbol
}
token1 {
id
symbol
}
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,
usdPrice: +position.pair.token0Price
},
token1: {
address: position.pair.token1.id,
balance: reserve1,
symbol: position.pair.token1.symbol,
usdPrice: +position.pair.token1Price
}
}
}))

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)

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<UserPosition[]> => {
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);
};
Loading
Loading