-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #56 from chef-jojo/add-pcs
Add PancakeSwap
- Loading branch information
Showing
6 changed files
with
418 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"name": "pancakeswap", | ||
"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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
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, | ||
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 = [3759558]; //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 v3Positions = await getV3UserPositionsAtBlock(blockNumber); | ||
|
||
const combinedPositions = 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'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 V3_SUBGRAPH_URL = | ||
'https://api.studio.thegraph.com/query/45376/exchange-v3-linea/version/latest'; | ||
|
||
export const client = createPublicClient({ | ||
chain: linea, | ||
transport: http('https://rpc.linea.build'), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { V3_SUBGRAPH_URL, client } from './config'; | ||
import { UserPosition } from './types'; | ||
|
||
type V3Position = { | ||
id: string; | ||
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, | ||
where: { liquidity_gt: 0, id_gt: ${skip} }, | ||
block: { number: ${blockNumber} }, | ||
orderBy: id | ||
) { | ||
id | ||
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 } = await response.json(); | ||
|
||
const { positions } = data; | ||
|
||
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 = positions[positions.length - 1].id; | ||
} | ||
} | ||
|
||
return result; | ||
}; | ||
|
||
export const getTimestampAtBlock = async (blockNumber: number) => { | ||
const block = await client.getBlock({ | ||
blockNumber: BigInt(blockNumber), | ||
}); | ||
return Number(block.timestamp * 1000n); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} | ||
} |
Oops, something went wrong.