-
Notifications
You must be signed in to change notification settings - Fork 85
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 #62 from spaceh3ad/main
added SpartaDEX support
- Loading branch information
Showing
7 changed files
with
657 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,28 @@ | ||
{ | ||
"name": "sparta-dex", | ||
"version": "1.0.0", | ||
"description": "", | ||
"main": "index.js", | ||
"type": "commonjs", | ||
"scripts": { | ||
"start": "node dist/index.js", | ||
"compile": "tsc", | ||
"debug": "ts-node src/index.ts", | ||
"watch": "tsc -w", | ||
"clear": "rm -rf dist", | ||
"test": "node " | ||
}, | ||
"keywords": [], | ||
"author": "", | ||
"license": "UNLICENSED", | ||
"dependencies": { | ||
"@apollo/client": "^3.9.11", | ||
"csv-writer": "^1.6.0", | ||
"react": "^18.2.0", | ||
"ts-node": "^10.9.2" | ||
}, | ||
"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,327 @@ | ||
import { createObjectCsvWriter } from "csv-writer"; | ||
import { | ||
client, | ||
PROTOCOL_DEPLOY_BLOCK, | ||
SNAPSHOT_PERIOD_BLOCKS, | ||
FIRST_TIME, | ||
POOL_TOKENS, | ||
} from "./sdk/config"; | ||
import { | ||
OutputDataSchemaRow, | ||
BlockData, | ||
UserPositions, | ||
Sync, | ||
Transaction, | ||
CumulativePositions, | ||
Reserves, | ||
UserReserves, | ||
UserPosition, | ||
} from "./sdk/types"; | ||
import { TRANSFERS_QUERY, SYNCS_QUERY } from "./sdk/queries"; | ||
import { | ||
getLatestBlockNumberAndTimestamp, | ||
getTimestampAtBlock, | ||
readLastProcessedBlock, | ||
saveLastProcessedBlock, | ||
} from "./sdk/utils"; | ||
import fs from "fs"; | ||
|
||
// Processes a block range to calculate user positions for mints and burns | ||
async function processBlockData(block: number): Promise<UserPosition[]> { | ||
// fetch lp transfers up to block | ||
const liquidityData = await fetchTransfers(block); | ||
const { userPositions, cumulativePositions } = | ||
processTransactions(liquidityData); | ||
|
||
// get reserves at block | ||
const reservesSnapshotAtBlock = await fetchReserves(block); | ||
|
||
// calculate tokens based on reserves | ||
const userReserves = calculateUserReservePortion( | ||
userPositions, | ||
cumulativePositions, | ||
reservesSnapshotAtBlock | ||
); | ||
|
||
const timestamp = await getTimestampAtBlock(block); | ||
|
||
// convert userReserves to userPositions | ||
return convertToUserPositions(userReserves, block, timestamp); | ||
} | ||
|
||
function convertToUserPositions( | ||
userData: UserReserves, | ||
block_number: number, | ||
timestamp: number | ||
): UserPosition[] { | ||
const tempResults: Record<string, UserPosition> = {}; | ||
|
||
Object.keys(userData).forEach((user) => { | ||
const contracts = userData[user]; | ||
Object.keys(contracts).forEach((contractId) => { | ||
const details = contracts[contractId]; | ||
|
||
// Process token0 | ||
const key0 = `${user}-${details.token0}`; | ||
if (!tempResults[key0]) { | ||
tempResults[key0] = { | ||
block_number, | ||
timestamp, | ||
user, | ||
token: details.token0, | ||
balance: details.amount0, | ||
}; | ||
} else { | ||
tempResults[key0].balance += details.amount0; | ||
} | ||
|
||
// Process token1 | ||
const key1 = `${user}-${details.token1}`; | ||
if (!tempResults[key1]) { | ||
tempResults[key1] = { | ||
block_number, | ||
timestamp, | ||
user, | ||
token: details.token1, | ||
balance: details.amount1, | ||
}; | ||
} else { | ||
tempResults[key1].balance += details.amount1; | ||
} | ||
}); | ||
}); | ||
|
||
// Convert the map to an array of UserPosition | ||
return Object.values(tempResults); | ||
} | ||
function calculateUserReservePortion( | ||
userPositions: UserPositions, | ||
totalSupply: CumulativePositions, | ||
reserves: Reserves | ||
): UserReserves { | ||
const userReserves: UserReserves = {}; | ||
|
||
Object.keys(userPositions).forEach((contractId) => { | ||
if ( | ||
!totalSupply[contractId] || | ||
!reserves[contractId] || | ||
!POOL_TOKENS[contractId] | ||
) { | ||
console.log(`Missing data for contract ID: ${contractId}`); | ||
return; | ||
} | ||
|
||
Object.keys(userPositions[contractId]).forEach((user) => { | ||
const userPosition = userPositions[contractId][user]; | ||
const total = totalSupply[contractId]; | ||
|
||
const share = userPosition / total; | ||
const reserve0 = parseInt(reserves[contractId].reserve0.toString()); | ||
const reserve1 = parseInt(reserves[contractId].reserve1.toString()); | ||
const token0 = POOL_TOKENS[contractId].token0; | ||
const token1 = POOL_TOKENS[contractId].token1; | ||
|
||
if (!userReserves[user]) { | ||
userReserves[user] = {}; | ||
} | ||
|
||
userReserves[user][contractId] = { | ||
amount0: BigInt(share * reserve0), | ||
amount1: BigInt(share * reserve1), | ||
token0: token0, | ||
token1: token1, | ||
}; | ||
}); | ||
}); | ||
|
||
return userReserves; | ||
} | ||
|
||
function processTransactions(transactions: Transaction[]): { | ||
userPositions: UserPositions; | ||
cumulativePositions: CumulativePositions; | ||
} { | ||
const userPositions: UserPositions = {}; | ||
const cumulativePositions: CumulativePositions = {}; | ||
|
||
transactions.forEach((transaction) => { | ||
// Normalize addresses for case-insensitive comparison | ||
const fromAddress = transaction.from.toLowerCase(); | ||
const toAddress = transaction.to.toLowerCase(); | ||
const contractId = transaction.contractId_.toLowerCase(); | ||
|
||
// Skip transactions where 'from' or 'to' match the contract ID, or both 'from' and 'to' are zero addresses | ||
if ( | ||
fromAddress === contractId || | ||
toAddress === contractId || | ||
(fromAddress === "0x0000000000000000000000000000000000000000" && | ||
toAddress === "0x0000000000000000000000000000000000000000") | ||
) { | ||
return; | ||
} | ||
|
||
// Initialize cumulativePositions if not already set | ||
if (!cumulativePositions[contractId]) { | ||
cumulativePositions[contractId] = 0; | ||
} | ||
|
||
// Convert the transaction value from string to integer. | ||
let value = parseInt(transaction.value.toString()); | ||
|
||
// Process transactions that increase liquidity (to address isn't zero) | ||
if (toAddress !== "0x0000000000000000000000000000000000000000") { | ||
if (!userPositions[contractId]) { | ||
userPositions[contractId] = {}; | ||
} | ||
if (!userPositions[contractId][toAddress]) { | ||
userPositions[contractId][toAddress] = 0; | ||
} | ||
userPositions[contractId][toAddress] += value; | ||
cumulativePositions[contractId] += value; | ||
} | ||
|
||
// Process transactions that decrease liquidity (from address isn't zero) | ||
if (fromAddress !== "0x0000000000000000000000000000000000000000") { | ||
if (!userPositions[contractId]) { | ||
userPositions[contractId] = {}; | ||
} | ||
if (!userPositions[contractId][fromAddress]) { | ||
userPositions[contractId][fromAddress] = 0; | ||
} | ||
userPositions[contractId][fromAddress] -= value; | ||
cumulativePositions[contractId] -= value; | ||
} | ||
}); | ||
|
||
return { userPositions, cumulativePositions }; | ||
} | ||
|
||
async function fetchTransfers(blockNumber: number) { | ||
const { data } = await client.query({ | ||
query: TRANSFERS_QUERY, | ||
variables: { blockNumber }, | ||
fetchPolicy: "no-cache", | ||
}); | ||
return data.transfers; | ||
} | ||
|
||
async function fetchReserves(blockNumber: number): Promise<Reserves> { | ||
const { data } = await client.query({ | ||
query: SYNCS_QUERY, | ||
variables: { blockNumber }, | ||
fetchPolicy: "no-cache", | ||
}); | ||
|
||
const latestPerContractId: Record<string, Sync> = {}; | ||
const reserves: Reserves = {}; | ||
|
||
data.syncs.forEach((sync: Sync) => { | ||
const existingEntry = latestPerContractId[sync.contractId_]; | ||
if ( | ||
!existingEntry || | ||
new Date(sync.timestamp_) > new Date(existingEntry.timestamp_) | ||
) { | ||
latestPerContractId[sync.contractId_] = sync; | ||
} | ||
}); | ||
|
||
Object.values(latestPerContractId).forEach((sync) => { | ||
reserves[sync.contractId_] = { | ||
reserve0: sync.reserve0, | ||
reserve1: sync.reserve1, | ||
}; | ||
}); | ||
|
||
return reserves; | ||
} | ||
|
||
function convertToOutputDataSchema( | ||
userPositions: UserPosition[] | ||
): OutputDataSchemaRow[] { | ||
return userPositions.map((userPosition) => { | ||
return { | ||
block_number: userPosition.block_number, | ||
timestamp: userPosition.timestamp, | ||
user_address: userPosition.user, | ||
token_address: userPosition.token, | ||
token_balance: BigInt(userPosition.balance), // Ensure balance is treated as bigint | ||
token_symbol: "", // You may want to fill this based on additional token info you might have | ||
usd_price: 0, // Adjust if you need to calculate this value or pull from another source | ||
}; | ||
}); | ||
} | ||
|
||
// Get block ranges for processing | ||
async function getBlockRangesToFetch() { | ||
const startBlock = FIRST_TIME | ||
? PROTOCOL_DEPLOY_BLOCK | ||
: readLastProcessedBlock(); | ||
|
||
if (!startBlock) { | ||
console.error("Failed to read last processed block"); | ||
return []; | ||
} | ||
|
||
const { blockNumber } = await getLatestBlockNumberAndTimestamp(); | ||
|
||
const blocks = []; | ||
for (let i = startBlock; i <= blockNumber; i += SNAPSHOT_PERIOD_BLOCKS) { | ||
blocks.push(i); | ||
} | ||
return blocks; | ||
} | ||
|
||
// Saves processed data to a CSV file | ||
async function saveToCSV(outputData: OutputDataSchemaRow[]) { | ||
const csvPath = "output.csv"; | ||
const fileExists = fs.existsSync(csvPath); | ||
|
||
const csvWriter = createObjectCsvWriter({ | ||
path: csvPath, | ||
header: [ | ||
{ id: "block_number", title: "Block Number" }, | ||
{ id: "timestamp", title: "Timestamp" }, | ||
{ id: "user_address", title: "User Address" }, | ||
{ id: "token_address", title: "Token Address" }, | ||
{ id: "token_balance", title: "Token Balance" }, | ||
{ id: "token_symbol", title: "Token Symbol" }, | ||
{ id: "usd_price", title: "USD Price" }, | ||
], | ||
append: fileExists, | ||
}); | ||
|
||
await csvWriter.writeRecords(outputData); | ||
console.log("CSV file has been written successfully"); | ||
} | ||
|
||
export const getUserTVLByBlock = async (blocks: BlockData) => { | ||
const data: UserPosition[] = await processBlockData(blocks.blockNumber); | ||
return convertToOutputDataSchema(data); | ||
}; | ||
|
||
async function main() { | ||
console.log(`Starting data fetching process mode: ${FIRST_TIME}`); | ||
const blocks = await getBlockRangesToFetch(); | ||
|
||
let lastblock = 0; | ||
try { | ||
for (const block of blocks) { | ||
const blockData = await getUserTVLByBlock({ | ||
blockNumber: block, | ||
blockTimestamp: 0, | ||
}); | ||
// userData.push(...blockData); | ||
console.log("Processed block", block); | ||
await saveToCSV(blockData); | ||
lastblock = block; | ||
} | ||
} catch (error: any) { | ||
console.error("Error processing block", lastblock, error.message); | ||
} finally { | ||
saveLastProcessedBlock(lastblock); | ||
} | ||
} | ||
|
||
// IMPORTANT: config::FIRST_TIME is set to true be default | ||
// after inital fetch set it to false | ||
main().catch(console.error); |
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,41 @@ | ||
import { ApolloClient, InMemoryCache } from "@apollo/client"; | ||
import { PoolTokens } from "./types"; | ||
|
||
export const SPARTA_SUBGRAPH_QUERY_URL = | ||
"https://api.goldsky.com/api/public/project_clv137yzf5wmt01w2bv2f4cgk/subgraphs/sparta-linea/1/gn"; | ||
|
||
export const LINEA_RPC = "https://rpc.linea.build"; | ||
|
||
export const client = new ApolloClient({ | ||
uri: SPARTA_SUBGRAPH_QUERY_URL, | ||
cache: new InMemoryCache(), | ||
}); | ||
|
||
// snpashot should be taken every 1 hour, average block time on linea is 11.5 seconds | ||
export const SNAPSHOT_PERIOD_BLOCKS = 311; | ||
export const PROTOCOL_DEPLOY_BLOCK = 3811977; | ||
|
||
export const FIRST_TIME = true; | ||
|
||
export const POOL_TOKENS: PoolTokens = { | ||
"0x0460c78bd496ca8e9483e4f0655a28be1e90a89b": { | ||
token0: "0x176211869ca2b568f2a7d4ee941e073a821ee1ff", | ||
token1: "0xa219439258ca9da29e9cc4ce5596924745e12b93", | ||
}, | ||
"0x30cc8a4f62f1c89bf4246196901e27982be4fd30": { | ||
token0: "0x11F98c7E42A367DaB4f200d2fdc460fb445CE9a8", | ||
token1: "0x176211869ca2b568f2a7d4ee941e073a821ee1ff", | ||
}, | ||
"0x51a056cc4eb7d1feb896554f97aa01805d41f190": { | ||
token0: "0x176211869ca2b568f2a7d4ee941e073a821ee1ff", | ||
token1: "0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f", | ||
}, | ||
"0x38d4b2627ff87911410129849246a1a19f586873": { | ||
token0: "0x3aab2285ddcddad8edf438c1bab47e1a9d05a9b4", | ||
token1: "0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f", | ||
}, | ||
"0x6a4d34cea32ecc5be2fc3ec97ce629f2b4c72334": { | ||
token0: "0x176211869ca2b568f2a7d4ee941e073a821ee1ff", | ||
token1: "0x580e933d90091b9ce380740e3a4a39c67eb85b4c", | ||
}, | ||
}; |
Oops, something went wrong.