Skip to content

Commit

Permalink
Merge pull request #294 from batphonghan/add_balancer_pendle
Browse files Browse the repository at this point in the history
kelp: gain :  tvl for  blancer, pendle, spectra users
  • Loading branch information
0xroll authored Sep 17, 2024
2 parents 9b2dcc5 + a5839b9 commit ccb8e33
Show file tree
Hide file tree
Showing 11 changed files with 6,069 additions and 1,120 deletions.
4 changes: 2 additions & 2 deletions adapters/kelp_gain_linea/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
"bignumber.js": "^9.1.2",
"csv-parser": "^3.0.0",
"decimal.js-light": "^2.5.1",
"ethereum-block-by-date": "^1.4.9",
"ethers": "^5.7.2",
"fast-csv": "^5.0.1",
"graphql": "^16.6.0",
"graphql-request": "^6.1.0",
"jsbi": "^4.3.0",
"tiny-invariant": "^1.3.1",
"toformat": "^2.0.0",
"ethereum-block-by-date": "^1.4.9"
"toformat": "^2.0.0"
},
"devDependencies": {
"@types/ethereum-block-by-date": "^1.4.1",
Expand Down
65 changes: 48 additions & 17 deletions adapters/kelp_gain_linea/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import fs from "fs";
import { write } from "fast-csv";
import csv from "csv-parser";
import {
agConvertToAssets,
agEthToRsEth,
agETHTotalLiquid,
getEtherumBlock,
getRsETHBalance,
getRsETHPrice,
getWRsETHBalance
getWRsETHBalance,
rsETHTotalSupply
} from "./lib/fetcher";
import { rsETH } from "./lib/utils";
import BigNumber from "bignumber.js";
Expand Down Expand Up @@ -40,6 +41,7 @@ const getMultiplierPercent = (tvlInUSD: BigNumber) => {
}
return 156;
};

export const getRsEthTVLInUSD = async (blockNumber: number) => {
const [rsETHBalanceRaw, wrsETHBalanceRaw, rsEthPrice] = await Promise.all([
getRsETHBalance(blockNumber),
Expand All @@ -63,37 +65,42 @@ export const getUserTVLByBlock = async (blocks: BlockData) => {
const { blockNumber, blockTimestamp } = blocks;

const ethBlockNumber = await getEtherumBlock(blockTimestamp);
const [tvl, agRate, agEthTotalSupply, allUser] = await Promise.all([
getRsEthTVLInUSD(blockNumber),
agConvertToAssets(ethBlockNumber),
agETHTotalLiquid(ethBlockNumber),
getAllAgEthHodlers(ethBlockNumber)
]);
console.log(`ETH BLOCK ${ethBlockNumber}, linea block: ${blockNumber}`);

const [tvl, agEthPerRsEthRate, agEthTotalSupply, allUser] = await Promise.all(
[
getRsEthTVLInUSD(blockNumber),
agEthToRsEth(ethBlockNumber),
agETHTotalLiquid(ethBlockNumber),
getAllAgEthHodlers(ethBlockNumber, blockTimestamp)
]
);

// Total rsETH deposit to mainnet
const mainnetTVLInRsETH =
BigInt(agEthTotalSupply * agRate) / BigInt(10 ** 18);
BigInt(agEthTotalSupply * agEthPerRsEthRate) / BigInt(10 ** 18);

const lineaToMainnetRatio =
(BigInt(tvl.lineaTVLInRsEth) * BigInt(10 ** 18)) /
BigInt(mainnetTVLInRsETH);

const mulPercent = getMultiplierPercent(tvl.tvlInUSD);
console.log(
`Ratio linea/mainnet ${ethers.utils.formatEther(
lineaToMainnetRatio
)}, lineaTVL: ${ethers.utils.formatEther(
)}, lineaTVL : ${ethers.utils.formatEther(
tvl.lineaTVLInRsEth
)} rsETH, mainnetTVL: ${ethers.utils.formatEther(mainnetTVLInRsETH)} rsETH`
)} rsETH, mainnetTVL: ${ethers.utils.formatEther(
mainnetTVLInRsETH
)} rsETH mulPercent: ${mulPercent}`
);
const csvRows: OutputDataSchemaRow[] = [];
const mulPercent = getMultiplierPercent(tvl.tvlInUSD);

allUser.forEach((item: UserBalanceSubgraphEntry) => {
const userBalanceAgEth = item.balance;
const userBalance = item.balance;
const balanceInRsEthRaw = BigInt(userBalance) * BigInt(agEthPerRsEthRate);
const mainnetUserBalanceRsEth =
(((BigInt(userBalanceAgEth) * BigInt(agRate)) / BigInt(10 ** 18)) *
BigInt(mulPercent)) /
100n;
((balanceInRsEthRaw / BigInt(10 ** 18)) * BigInt(mulPercent)) / 100n;

const lineaUserBalance =
(lineaToMainnetRatio * mainnetUserBalanceRsEth) / BigInt(10 ** 18);
Expand All @@ -109,6 +116,27 @@ export const getUserTVLByBlock = async (blocks: BlockData) => {
});
});

let totalRsEthSaveToCSV = csvRows.reduce(
(acc, s) => acc + s.token_balance,
0n
);

const rsEthTotalSupply = await rsETHTotalSupply(blockNumber);

console.log(
`TOTAL rsEth balance in CSV : ${ethers.utils.formatEther(
totalRsEthSaveToCSV.toString()
)}\nTOTAL rsETH supply linea: ${ethers.utils.formatEther(
rsEthTotalSupply.toString()
)}`
);

if (totalRsEthSaveToCSV > rsEthTotalSupply) {
throw new Error(
`The total balance in CSV ${totalRsEthSaveToCSV} can not more than total supply ${rsEthTotalSupply}`
);
}

return csvRows;
};

Expand Down Expand Up @@ -148,7 +176,10 @@ readBlocksFromCSV("hourly_blocks.csv")
const result = await getUserTVLByBlock(block);
allCsvRows.push(...result);
} catch (error) {
console.error(`An error occurred for block ${block}:`, error);
console.error(
`An error occurred for block ${JSON.stringify(block)}:`,
error
);
}
}
await new Promise((resolve, reject) => {
Expand Down
135 changes: 135 additions & 0 deletions adapters/kelp_gain_linea/src/lib/balancer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { gql } from "graphql-request";
import { ethers } from "ethers";
import { subgraphFetchAllById, subgraphFetchOne } from "./query";
import { BALANCER_START_BLOCK } from "./utils";

const BALANCER_V2_ENDPOINT =
"https://api.thegraph.com/subgraphs/id/QmQ5TT2yYBZgoUxsat3bKmNe5Fr9LW9YAtDs8aeuc1BRhj";
const AGETH_POOL_ID =
"0xf1bbc5d95cd5ae25af9916b8a193748572050eb00000000000000000000006bc";
interface GraphQLQuery {
query: string;
collection: string;
}

interface UserAddress {
id: string;
}

interface Share {
id: string;
userAddress: UserAddress;
balance: string;
}

interface Token {
priceRate: string; // or number, depending on the actual data type
weight: string; // or number
balance: string; // or number
symbol: string;
}

interface Pool {
tokens: Token[];
totalShares: string;
}

interface GetPoolDetailsResponse {
pool: Pool;
}

const BALANCER_POOL_SHARES_QUERY: GraphQLQuery = {
query: gql`
query GetPoolShares($poolId: ID!, $block: Int, $lastId: ID!) {
poolShares(
where: {
poolId: $poolId
id_gt: $lastId
balance_gt: "0"
userAddress_not: "0x0000000000000000000000000000000000000000"
}
block: { number: $block }
first: 1000
orderBy: id
orderDirection: asc
) {
id
balance
userAddress {
id
}
}
}
`,
collection: "poolShares"
};

const POOL_DETAILS_QUERY: GraphQLQuery = {
query: gql`
query GetPoolDetails($poolId: ID!, $block: Int) {
pool(id: $poolId, block: { number: $block }) {
tokens {
priceRate
weight
balance
symbol
}
totalShares
}
}
`,
collection: "pool"
};

export async function getPoolDetails(block: number): Promise<Pool> {
return await subgraphFetchOne<Pool>(
BALANCER_V2_ENDPOINT,
POOL_DETAILS_QUERY.query,
POOL_DETAILS_QUERY.collection,
{ poolId: AGETH_POOL_ID, block: block }
);
}

export async function fetchBalancerAgEthPoolShares(
block: number
): Promise<Share[]> {
return await subgraphFetchAllById<Share>(
BALANCER_V2_ENDPOINT,
BALANCER_POOL_SHARES_QUERY.query,
BALANCER_POOL_SHARES_QUERY.collection,
{ poolId: AGETH_POOL_ID, block: block }
);
}

function convertLpToAgETH(balances: Share[], poolDetails: Pool) {
const agETH = poolDetails.tokens.filter(
(token) => token.symbol == "agETH"
)[0];
const totalPoolAgETH = ethers.utils.parseEther(agETH.balance).toBigInt();
const totalLiquidity = ethers.utils
.parseEther(poolDetails.totalShares)
.toBigInt();

for (let i = 0; i < balances.length; i++) {
const userLpBalance = ethers.utils
.parseEther(balances[i].balance)
.toBigInt();
const userAgETH = (userLpBalance * totalPoolAgETH) / totalLiquidity;
balances[i].balance = userAgETH.toString();
}
return balances;
}

export async function fetchAllBalancerShare(blockNumber: number) {
if (blockNumber < BALANCER_START_BLOCK) {
return [];
}
let balances = await fetchBalancerAgEthPoolShares(blockNumber);
const poolDetails = await getPoolDetails(blockNumber);
let shares = convertLpToAgETH(balances, poolDetails);

if (shares.length == 0) {
throw new Error(`Empty share Spectra BLOCK: ${blockNumber}`);
}
return shares;
}
84 changes: 82 additions & 2 deletions adapters/kelp_gain_linea/src/lib/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,18 @@ import {
agETH
} from "./utils";

export async function getEtherumBlock(blockTimestampSecs: number) {
export async function getEtherumBlock(
blockTimestampSecs: number
): Promise<number> {
return retry({
fn: async () => {
return await _getEtherumBlock(blockTimestampSecs);
},
name: `_getEtherumBlock`
});
}

export async function _getEtherumBlock(blockTimestampSecs: number) {
const blockTimestampInMill = blockTimestampSecs * 1000;
const date = new Date(blockTimestampInMill); //
// External API
Expand All @@ -32,6 +43,14 @@ export async function agETHTotalLiquid(blockNumber: number): Promise<bigint> {
return totalSupply - locked;
}

export async function rsETHTotalSupply(blockNumber: number): Promise<bigint> {
let totalSupply = await rsETHContract.totalSupply({
blockTag: blockNumber
});

return totalSupply;
}

async function agETHTotalSupply(blockNumber: number): Promise<bigint> {
let totalSupply = await agETHContract.totalSupply({
blockTag: blockNumber
Expand All @@ -48,6 +67,17 @@ async function agETHTotalLocked(blockNumber: number): Promise<bigint> {
return lockedAmount;
}

export async function agETHBalancerOf(
blockNumber: number,
address: string
): Promise<bigint> {
let balance = await agETHContract.balanceOf(address, {
blockTag: blockNumber
});

return balance;
}

export async function getRsETHBalance(blockNumber: number): Promise<bigint> {
let rsETHBalance = await rsETHContract.balanceOf(kelpGAINLinea, {
blockTag: blockNumber
Expand Down Expand Up @@ -87,13 +117,23 @@ async function decimals(blockNumber: number): Promise<string> {
return decimals;
}

export async function agConvertToAssets(blockNumber: number): Promise<bigint> {
// Giving rsETH, return agETH
export async function rsEthToAgEth(blockNumber: number): Promise<bigint> {
const rate = await agETHContract.convertToShares(BigInt(10 ** 18), {
blockTag: blockNumber
});

return rate;
}
// Giving agETH, return rsETH
export async function agEthToRsEth(blockNumber: number): Promise<bigint> {
const rate = await agETHContract.convertToAssets(BigInt(10 ** 18), {
blockTag: blockNumber
});

return rate;
}

export async function getRsETHPrice(blockNumber: number): Promise<BigNumber> {
const [rsEthRateRaw, ethPriceRaw, ethPriceDec] = await Promise.all([
rsETHRate(blockNumber),
Expand All @@ -108,3 +148,43 @@ export async function getRsETHPrice(blockNumber: number): Promise<BigNumber> {

return rsEthRate.times(ethPrice);
}

/**
* A wrapper function that retries a function that returns a promise of some resource
* @param fn - The function that returns a promise of some resource
* @param retries - The number of times to retry the function
* @param delayInSecs - The delay between retries in seconds
* @returns - A promise of the resource
*/
export async function retry<T>({
fn,
retries = 10,
delayInSecs = 1000,
name = "Function"
}: SimpleRetry<T>): Promise<T> {
let currentAttempt = 0;
do {
try {
const res = await fn();
return res as T;
} catch (error) {
currentAttempt++;
console.log(
`Error in retry(${name}): Retry count ${currentAttempt}`,
error
);
}
await wait(delayInSecs);
} while (currentAttempt <= retries);

throw new Error(`Error in retry(${name})`);
}

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

type SimpleRetry<T> = {
fn: () => T | Promise<T>;
retries?: number;
delayInSecs?: number;
name?: string;
};
Loading

0 comments on commit ccb8e33

Please sign in to comment.