Skip to content

Commit

Permalink
Merge pull request #62 from spaceh3ad/main
Browse files Browse the repository at this point in the history
added SpartaDEX support
  • Loading branch information
nitish-91 authored Apr 25, 2024
2 parents 4537785 + eac7aec commit 24871fa
Show file tree
Hide file tree
Showing 7 changed files with 657 additions and 0 deletions.
28 changes: 28 additions & 0 deletions adapters/sparta/package.json
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"
}
}
327 changes: 327 additions & 0 deletions adapters/sparta/src/index.ts
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);
41 changes: 41 additions & 0 deletions adapters/sparta/src/sdk/config.ts
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",
},
};
Loading

0 comments on commit 24871fa

Please sign in to comment.